用来记录一些原创性的总结
假设现在有这样一项任务,要求你写两个关于获取User和Dept的接口出来,它们对外提供的数据是XML格式,分别对应如下格式。
<response>
<errorCode>0</errorCode>
<errorMessage>成功</errorMessage>
<data>
<dept>
<id>100</id>
<name>财务部</name>
<parentId>10</parentId>
<no>A001</no>
</dept>
</data>
</response>
<response>
<errorCode>0</errorCode>
<errorMessage>成功</errorMessage>
<data>
<user>
<id>1000</id>
<name>张三</name>
<parentId>100</parentId>
<username>zhangsan</username>
</user>
</data>
</response>
从response到data那部分其实是系统中对外接口的通用格式,所不同的内容都是data元素里面包含的内容,不单是上面两个接口的响应格式,其它接口也是一样的。通用格式如下所示:
<response>
<errorCode>0</errorCode>
<errorMessage>成功</errorMessage>
<data>
<!--特性的内容-->
</data>
</response>
关于User和Dept接口对应的Java类,系统中已经存在了相应的定义,定义如下:
/**
* 抽象的组织机构,提取出公用的id、父级id和名称
*/
public abstract class AbstractOrg {
public static final int TYPE_DEPT = 1;
public static final int TYPE_USER = 2;
private Integer id;
private Integer parentId;
private String name;
/**
* 区分组织类型的,1是Dept,2是User
*/
private final int type;
protected AbstractOrg(int type) {
this.type = type;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getType() {
return type;
}
}
public class Dept extends AbstractOrg {
private String no;
public Dept() {
super(AbstractOrg.TYPE_DEPT);
}
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
}
public class User extends AbstractOrg {
private String username;
public User() {
super(AbstractOrg.TYPE_USER);
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
为了满足上述的接口需求,通用的格式对应的Java类定义我们可能会定义如下,定义了Response类和ResultData类,真实的data部分将由ResultData持有。
/**
* 根节点
*/
@XmlRootElement
@XmlType(propOrder= {"errorCode", "errorMessage", "data"})
public class Response {
private String errorCode;
private String errorMessage;
private ResultData data;
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public ResultData getData() {
return data;
}
public void setData(ResultData data) {
this.data = data;
}
}
/**
* data节点包装对象的包装类
*/
public class ResultData {
private Object realData;
public ResultData() {}
public ResultData(Object realData) {
this.realData = realData;
}
public void setRealData(Object realData) {
this.realData = realData;
}
@XmlElement
public Object getRealData() {
return realData;
}
}
这样Dept对应的接口生成的XML可以进行类似如下的调用,在创建对应的JAXBContext时必须传递realData对应的真实Class,因为通过Response关联到的ResultData中定义的realData的类型是java.lang.Object。不传递realData的真实类型时,JAXB遇到realData会不知道如何去解析它。
@Test
public void testMarshalDept() throws Exception {
Dept dept = new Dept();
dept.setId(100);
dept.setParentId(10);
dept.setName("财务部");
dept.setNo("A001");
this.marshal(dept);
}
private void marshal(Object realData) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(Response.class, realData.getClass());
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
Response response = new Response();
response.setErrorCode("0");
response.setErrorMessage("成功");
ResultData data = new ResultData(realData);
response.setData(data);
marshaller.marshal(response, System.out);
}
上面的测试代码生成的XML如下:
<response>
<errorCode>0</errorCode>
<errorMessage>成功</errorMessage>
<data>
<realData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="dept">
<id>100</id>
<name>财务部</name>
<parentId>10</parentId>
<no>A001</no>
</realData>
</data>
</response>
从生成的XML我们可以看到生成的XML的结构确实是按照传递的realData的真实类型的结构生成的,比如上面就是按照传递的Dept类型的对应的结构生成的。但是有两个问题,一是realData节点上多了namespace的引用,二是realData节点的名称不是我们想要的dept,而是默认的realData。对于问题一来说,因为顺着根节点类型Response是没有直接关联Dept的,相对于Response来说,Dept是外部引入的,不是它内部直接关联的,所以在realData节点上有了类型的声明。对于问题二有三种解决方案,这三种解决方案都可以同时解决问题一和问题2,本文将介绍其中的两种方案,第三种方案在后续介绍动态根据对象属性生成节点名称时会讲到。
可能有读者会想到@XmlElement可以用来指定生成XML时元素的名称,而不是使用默认的属性名称。可能会想我们可以把ResultData上的@XmlElement改为@XmlElement(name=”dept”),这样生成的节点名称就是dept了。
public class ResultData {
private Object realData;
public ResultData() {}
public ResultData(Object realData) {
this.realData = realData;
}
public void setRealData(Object realData) {
this.realData = realData;
}
@XmlElement(name="dept")
public Object getRealData() {
return realData;
}
}
调整后生成的XML如下:
<response>
<errorCode>0</errorCode>
<errorMessage>成功</errorMessage>
<data>
<dept xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="dept">
<id>100</id>
<name>财务部</name>
<parentId>10</parentId>
<no>A001</no>
</dept>
</data>
</response>
这样看着好像是解决了节点名称的问题,实际上还是不可行的,上述我们把节点名称写死了,已经明确的指定为dept了,如果realData传递的是User类型的对象,生成的节点名称也会是dept,而不是我们期望的user。所以这么做肯定是不行的。需要动态的使用节点的名称。这个时候我们就可以把@XmlElement和@XmlElements一起使用,即定义多个@XmlElement,指定在realData为Dept类型时,生成的节点名称为dept,realData为User类型时,生成的节点名称为user。对ResultData的realData配置调整如下。
public class ResultData {
private Object realData;
public ResultData() {}
public ResultData(Object realData) {
this.realData = realData;
}
public void setRealData(Object realData) {
this.realData = realData;
}
@XmlElements({ @XmlElement(name = "dept", type = Dept.class), @XmlElement(name = "user", type = User.class) })
public Object getRealData() {
return realData;
}
}
这时候生成出来的XML就是我们期望的样子。传递Dept对象生成的结果如下:
<response>
<errorCode>0</errorCode>
<errorMessage>成功</errorMessage>
<data>
<dept>
<id>100</id>
<name>财务部</name>
<parentId>10</parentId>
<no>A001</no>
</dept>
</data>
</response>
传递User对象生成的结果如下:
<response>
<errorCode>0</errorCode>
<errorMessage>成功</errorMessage>
<data>
<user>
<id>1000</id>
<name>张三</name>
<parentId>100</parentId>
<username>zhangsan</username>
</user>
</data>
</response>
此方案的完整配置如下:
/**
* 根节点
*/
@XmlRootElement
@XmlType(propOrder= {"errorCode", "errorMessage", "data"})
public class Response {
private String errorCode;
private String errorMessage;
private ResultData data;
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public ResultData getData() {
return data;
}
public void setData(ResultData data) {
this.data = data;
}
}
/**
* data节点包装对象的包装类
*/
public class ResultData {
private Object realData;
public ResultData() {}
public ResultData(Object realData) {
this.realData = realData;
}
public void setRealData(Object realData) {
this.realData = realData;
}
@XmlElements({ @XmlElement(name = "dept", type = Dept.class), @XmlElement(name = "user", type = User.class) })
public Object getRealData() {
return realData;
}
}
/**
* 抽象的组织机构,提取出公用的id、父级id和名称
*/
public abstract class AbstractOrg {
public static final int TYPE_DEPT = 1;
public static final int TYPE_USER = 2;
private Integer id;
private Integer parentId;
private String name;
/**
* 区分组织类型的,1是Dept,2是User
*/
private final int type;
protected AbstractOrg(int type) {
this.type = type;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getType() {
return type;
}
}
public class Dept extends AbstractOrg {
private String no;
public Dept() {
super(AbstractOrg.TYPE_DEPT);
}
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
}
public class User extends AbstractOrg {
private String username;
public User() {
super(AbstractOrg.TYPE_USER);
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
使用此方案时我们在创建JAXBContext时可以不用再传递realData真实的类型了,因为它们都已经通过getRealData()方法上的@XmlElements注解定义了,JAXBContext通过ResultData就可以识别它们了。另外此方案在realData的类型比较少的时候可以很方便的进行配置,如果在一个比较大型的接口系统中,realData会有好几百或更多的可能性,把它们都定义在realData上有点不太现实。幸好接下来要介绍的方案二可以解决这种问题。
方案二是使用@XmlElementRef注解,使用该注解标注在getRealData()方案上可以使用JAXB在生成XML时自动使用当前定义类型的实际子类型的结构来生成。但是它标注的属性或方法的返回类型不能是java.lang.Object,需要是更加具体的类型。通常这种情况下我们会定义一个抽象类来供所有的realData类型继承,这个类型只是一个标示作用,可以是空的,即没有任何属性定义在其中。这里为了简便起见,我们使用Dept和User的父类AbstractOrg来代替。调整后的ResultData定义如下:
public class ResultData {
private AbstractOrg realData;
public ResultData() {}
public ResultData(AbstractOrg realData) {
this.realData = realData;
}
public void setRealData(AbstractOrg realData) {
this.realData = realData;
}
@XmlElementRef
public AbstractOrg getRealData() {
return realData;
}
}
使用@XmlElementRef后整个节点的生成都将依赖于实际的子类型,包括根节点的名称,此时的根节点名称需要在子类型上通过@XmlRootElement来指定,而不再默认使用持有它的对象对应的属性的名称,即不再是默认的realData。
@XmlRootElement(name="dept")
public class Dept extends AbstractOrg {
private String no;
public Dept() {
super(AbstractOrg.TYPE_DEPT);
}
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
}
@XmlRootElement
public class User extends AbstractOrg {
private String username;
public User() {
super(AbstractOrg.TYPE_USER);
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
采用@XmlElementRef的方式时节点名称只能在子类上通过@XmlRootElement指定,不能再在对应的属性上通过@XmlElement来指定其它名称了,即上述的示例中不能在getRealData()方法上既有@XmlElementRef,又有@XmlElement,因为@XmlElementRef和@XmlElement是互斥的。
采用此种方式在创建JAXBContext时也需要传递本次进行XML和Java对象相互转换使用到的realData的实际类型,否则具体的实际类型将不被识别。如果期望JAXBContext时不传递realData的实际类型,也可以在JAXBContext能够识别的Class上通过@XmlSeeAlso来引入。通常是加在父类上的。
@XmlSeeAlso({Dept.class, User.class})
public abstract class AbstractOrg {
public static final int TYPE_DEPT = 1;
public static final int TYPE_USER = 2;
private Integer id;
private Integer parentId;
private String name;
/**
* 区分组织类型的,1是Dept,2是User
*/
private final int type;
protected AbstractOrg(int type) {
this.type = type;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getType() {
return type;
}
}
这种方式也不是万能的。有的时候我们的Java类是有继承关系的,在生成XML时也期望可以按照子类的结构生成XML,也使用了@XmlElementRef,但是我们期望生成的XML的节点的名称都是相同的,比如上述示例如果期望无论是Dept还是User,对应的节点名称都是org。这时候如果我们有通过XML转换为Java对象的需求,那么XML的org节点将转换为Java的Dept对象还是User对象呢?实际上这时候JAXB将按照定义的顺序来,后面的将具有更高的优先级,但是这很明显不满足我们的需求。所以此种情形下我们要避免使用@XmlSeeAlso把所有的子类型都引入,具体的子类型还是在创建JAXBContext时老老实实的传入。
如果需要反过来通过XML生成Java对象也是可以的,JAXB也能正确的识别需要创建的对象。为了验证这个问题,笔者做了如下实验。把marshal的结果,即通过Java对象生成的XML保存起来,然后再通过unmarshal把对应的XML转换为Java对象,即Response对象,再通过Response.getData().getRealData()获取到转换后的realData,把转换后的realData和转换前的realData做比较。注意这时候不能用==,需要使用equals,因为它们肯定是两个不同的对象。
@Test
public void testMarshalUser() throws Exception {
User user = new User();
user.setId(1000);
user.setParentId(100);
user.setName("张三");
user.setUsername("zhangsan");
String xml = this.marshal(user);
Response response = JAXB.unmarshal(new StringReader(xml), Response.class);
AbstractOrg realData = response.getData().getRealData();
Assert.assertEquals(user, realData);
}
@Test
public void testMarshalDept() throws Exception {
Dept dept = new Dept();
dept.setId(100);
dept.setParentId(10);
dept.setName("财务部");
dept.setNo("A001");
String xml = this.marshal(dept);
Response response = JAXB.unmarshal(new StringReader(xml), Response.class);
AbstractOrg realData = response.getData().getRealData();
Assert.assertEquals(dept, realData);
}
private String marshal(AbstractOrg realData) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(Response.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
Response response = new Response();
response.setErrorCode("0");
response.setErrorMessage("成功");
ResultData data = new ResultData(realData);
response.setData(data);
StringWriter writer = new StringWriter();
marshaller.marshal(response, writer);
return writer.toString();
}
为了使上述的测试结果能够顺利通过,还必须重写对应的Dept和User的equals方法,还需要在父类AbstractOrg上定义一个类似于equals那样比较所有属性值的方法以供子类在进行equals比较时调用。使用@XmlElementRef方式的并验证Java对象和XML能够完美相互转换的完整配置代码如下:
/**
* 根节点
*/
@XmlRootElement
@XmlType(propOrder= {"errorCode", "errorMessage", "data"})
public class Response {
private String errorCode;
private String errorMessage;
private ResultData data;
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public ResultData getData() {
return data;
}
public void setData(ResultData data) {
this.data = data;
}
}
/**
* data节点包装对象的包装类
*/
public class ResultData {
private AbstractOrg realData;
public ResultData() {}
public ResultData(AbstractOrg realData) {
this.realData = realData;
}
public void setRealData(AbstractOrg realData) {
this.realData = realData;
}
@XmlElementRef
public AbstractOrg getRealData() {
return realData;
}
}
/**
* 抽象的组织机构,提取出公用的id、父级id和名称
*/
@XmlSeeAlso({Dept.class, User.class})
public abstract class AbstractOrg {
public static final int TYPE_DEPT = 1;
public static final int TYPE_USER = 2;
private Integer id;
private Integer parentId;
private String name;
/**
* 区分组织类型的,1是Dept,2是User
*/
private final int type;
protected AbstractOrg(int type) {
this.type = type;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getType() {
return type;
}
protected boolean superEquals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AbstractOrg other = (AbstractOrg) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (parentId == null) {
if (other.parentId != null)
return false;
} else if (!parentId.equals(other.parentId))
return false;
if (type != other.type)
return false;
return true;
}
}
@XmlRootElement(name="dept")
public class Dept extends AbstractOrg {
private String no;
public Dept() {
super(AbstractOrg.TYPE_DEPT);
}
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Dept other = (Dept) obj;
if (no == null) {
if (other.no != null)
return false;
} else if (!no.equals(other.no))
return false;
if (!this.superEquals(obj)) {//父结构不能equals
return false;
}
return true;
}
}
@XmlRootElement
public class User extends AbstractOrg {
private String username;
public User() {
super(AbstractOrg.TYPE_USER);
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
if (!this.superEquals(obj)) {//父结构不能equals
return false;
}
return true;
}
}
以上就是以子类结构绑定XML的两种方式。