Elim的博客

用来记录一些原创性的总结


Elim

JAXB生成XML时指定以子类的结构生成XML

提出问题

假设现在有这样一项工作,要求你写两个接口出来,它们对外提供的数据是XML格式,分别对应如下格式。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<response>
    <message>AAAA</message>
    <data>
        <dept id="3">
            <name>技术部</name>
            <no>003001</no>
        </dept>
    </data>
    <returnCode>100</returnCode>
</response>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<response>
    <message>AAAA</message>
    <data>
        <person id="1">
            <name>张三</name>
        </person>
    </data>
    <returnCode>100</returnCode>
</response>

分析问题

经过分析我们可以发现这两个接口的返回格式是类似的,不同的是resultData元素包裹的内容是不一样的。为此,如果我们使用JAXB进行XML的格式化时我们就会想到定义好response元素对应的类,抽象出其中的data节点下面的元素。为此你可能会定义出如下这些类。

@XmlRootElement
class Response {

	private int returnCode;
	private String message;
	private ResultData resultData;

	public int getReturnCode() {
		return returnCode;
	}

	public void setReturnCode(int returnCode) {
		this.returnCode = returnCode;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	@XmlElement(name = "data")
	public ResultData getResultData() {
		return resultData;
	}

	public void setResultData(ResultData resultData) {
		this.resultData = resultData;
	}

}
class ResultData {

	private Data realData;

	public Data getRealData() {
		return realData;
	}

	public void setRealData(Data realData) {
		this.realData = realData;
	}

}
class Data {

}
class Person extends Data {
	private Integer id;
	private String name;

	@XmlAttribute
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}
@XmlRootElement
class Dept extends Data {
	private Integer id;
	private String no;
	private String name;

	@XmlAttribute
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getNo() {
		return no;
	}

	public void setNo(String no) {
		this.no = no;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

然后进行如下这样的测试。

public class XmlElementRefTest {

	@Test
	public void testPerson() throws Exception {
		Person person = new Person();
		person.setId(1);
		person.setName("张三");
		Response response = this.newResponse(person);
		this.marshal(response, Response.class, Person.class);
	}

	private void marshal(Response response, Class<?>... classesToBeBound) throws Exception {
		JAXBContext jaxbContext = JAXBContext.newInstance(classesToBeBound);
		Marshaller marshaller = jaxbContext.createMarshaller();
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		marshaller.marshal(response, System.out);
	}

	private Response newResponse(Data realData) {
		Response response = new Response();
		response.setReturnCode(100);
		response.setMessage("AAAA");
		ResultData resultData = new ResultData();
		resultData.setRealData(realData);
		response.setResultData(resultData);
		return response;
	}

}

然而,很不幸的是你会得到如下这样的结果。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<response>
    <message>AAAA</message>
    <data>
        <realData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="person" id="1">
            <name>张三</name>
        </realData>
    </data>
    <returnCode>100</returnCode>
</response>

解决问题

从上面的结果我们可以看到,生成XML时确实用的是子类对象Person,但是对应的节点名称还是默认的属性名称,即realData。那有什么办法可以使元素名称由realData变为persondept,同时又去掉那些xmlns:xsi等属性呢?这时候就可以用到XmlElementRef注解了,把它标注在ResultDatagetRealData()方法上,这就告诉JAXB,在进行XML转换时不要以realData属性的声明类型Data进行转换,而是以传递进来的真实类型进行转换。使用了XmlElementRef标注后,对应的属性生成的XML元素名称已经不再由当前属性来确定了,将由实际传入类型上的XmlRootElement指定的名称决定。所以我们还需要在Person类上使用XmlRootElement注解进行标注,可以通过其name属性来指定生成的XML元素的名称,不指定时默认将使用类名称,首字母小写。如果忘记在Person类上加上XmlRootElement注解了,那么你会遇到如下这样的错误。

com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
@XmlElementRef 无效: 类型 "class xxx.Data" 或其任意子类对此上下文是未知的。

调整后我们的ResultDataPerson类定义变成如下这样。

class ResultData {

	private Data realData;

	@XmlElementRef
	public Data getRealData() {
		return realData;
	}

	public void setRealData(Data realData) {
		this.realData = realData;
	}

}
@XmlRootElement
class Person extends Data {
	private Integer id;
	private String name;

	@XmlAttribute
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}

继续运行我们的测试类,你将得到如下这样的输出。这时候输出结果就跟我们预想的一样了。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<response>
    <message>AAAA</message>
    <data>
        <person id="1">
            <name>张三</name>
        </person>
    </data>
    <returnCode>100</returnCode>
</response>

同理,对于Dept类而言,我们也可以在其上面使用XmlElementRef注解进行标注。

需要注意的是:在进行marshal时,上下文类环境中一定要加上当前的子类型,即进行Person类转换时就加上Person.class,进行Dept类转换时就加上Dept.class。它们对应于测试代码中的classesToBeBound。如果没有指定则将出现异常。

以下是正常的、完整的示例代码。

public class XmlElementRefTest {

	@Test
	public void testPerson() throws Exception {
		Person person = new Person();
		person.setId(1);
		person.setName("张三");
		Response response = this.newResponse(person);
		this.marshal(response, Response.class, Person.class);
	}

	@Test
	public void testDept() throws Exception {
		Dept dept = new Dept();
		dept.setId(3);
		dept.setNo("003001");
		dept.setName("技术部");
		Response response = this.newResponse(dept);
		this.marshal(response, Response.class, Dept.class);
	}

	private void marshal(Response response, Class<?>... classesToBeBound) throws Exception {
		JAXBContext jaxbContext = JAXBContext.newInstance(classesToBeBound);
		Marshaller marshaller = jaxbContext.createMarshaller();
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		marshaller.marshal(response, System.out);
	}

	private Response newResponse(Data realData) {
		Response response = new Response();
		response.setReturnCode(100);
		response.setMessage("AAAA");
		ResultData resultData = new ResultData();
		resultData.setRealData(realData);
		response.setResultData(resultData);
		return response;
	}

}

@XmlRootElement
class Response {

	private int returnCode;
	private String message;
	private ResultData resultData;

	public int getReturnCode() {
		return returnCode;
	}

	public void setReturnCode(int returnCode) {
		this.returnCode = returnCode;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	@XmlElement(name = "data")
	public ResultData getResultData() {
		return resultData;
	}

	public void setResultData(ResultData resultData) {
		this.resultData = resultData;
	}

}

class ResultData {

	private Data realData;

	@XmlElementRef
	public Data getRealData() {
		return realData;
	}

	public void setRealData(Data realData) {
		this.realData = realData;
	}

}

class Data {

}

@XmlRootElement
class Person extends Data {
	private Integer id;
	private String name;

	@XmlAttribute
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}

@XmlRootElement
class Dept extends Data {
	private Integer id;
	private String no;
	private String name;

	@XmlAttribute
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getNo() {
		return no;
	}

	public void setNo(String no) {
		this.no = no;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

(注:本文由Elim写于2017年6月19日)