Elim的博客

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


Elim

JAXB动态指定生成的XML元素名称

通常我们在使用JAXB生成XML时,都是通过@XmlRootElement@XmlElement事先指定对应的类型的对象在生成XML时生成的元素的名称。比如下面这样。

public class SpecifyNameTest {

	@Test
	public void test() {
		User user = new User();
		user.setId(2);
		user.setName("张三");
		JAXB.marshal(user, System.out);
	}
	
	@XmlRootElement(name="User")
	public static class User {
		private Integer id;
		private String name;
		
		@XmlAttribute(name="id")
		public Integer getId() {
			return id;
		}
		
		public void setId(Integer id) {
			this.id = id;
		}
		
		@XmlElement(name="Name")
		public String getName() {
			return name;
		}
		
		public void setName(String name) {
			this.name = name;
		}
		
	}
	
}

对应的输出结果是如下这样:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<User id="2">
    <Name>张三</Name>
</User>

动态生成根节点元素名称

那如果现在业务上有这样一项需求,要求用户ID能够被2整除时生成的Xml根节点是“user_2”,否则生成的根节点是“user_1”(笔者这里只是简单的打个比方而已,虽然这个举例非常不切实际,但是实际的业务场景下可能由于遗留原因等情况,是会有动态生成Xml元素名称的需求的)。那这个时候应该怎么办呢?这个时候我们应该把我们的对象转换为一个JAXBElement对象,JAXB在把JAXBElement类型的对象转换为XML时会调用它的getName()方法获取对应的元素的名称。具体示例如下。

public class SpecifyNameTest {

	@Test
	public void test1() {
		ObjectFactory objectFactory = new ObjectFactory();
		User user = new User();
		user.setId(2);
		user.setName("张三");
		JAXBElement<User> userEle = objectFactory.createUser(user);
		JAXB.marshal(userEle, System.out);
	}
	
	public static class ObjectFactory {
		
		public JAXBElement<User> createUser(User user) {
			String localPart = null;//元素名称
			if (user.getId()%2 == 0) {
				localPart = "user_2";
			} else {
				localPart = "user_1";
			}
			QName name = new QName(localPart);
			JAXBElement<User> userEle = new JAXBElement<>(name, User.class, user);
			return userEle;
		}
		
	}
	
	@XmlRootElement(name="User")
	public static class User {
		private Integer id;
		private String name;
		
		@XmlAttribute(name="id")
		public Integer getId() {
			return id;
		}
		
		public void setId(Integer id) {
			this.id = id;
		}
		
		@XmlElement(name="Name")
		public String getName() {
			return name;
		}
		
		public void setName(String name) {
			this.name = name;
		}
		
	}
	
}

这个时候User类上的@XmlRootElement已经不起作用了,可有可无,但是属性上的@XmlAttribute@XmlElement还是会起作用的。生成的XML如下:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<user_2 id="2">
    <Name>张三</Name>
</user_2>

动态生成非根节点元素名称

有时候可能我们的一个类里面的某个属性生成的XML元素也需要动态化,那应该怎么做呢?现假设我们在原来的User类上增加一个deptName属性,我们希望在生成它对应的XML元素时,当用户的ID能被2整除时,生成的元素名称为“dept_2”,否则为“dept_1”。那你会不会觉得这很简单,我把对应的属性定义为JAXBElement类型即可,像如下这样子。

	public static class ObjectFactory {
		
		public JAXBElement<String> createDept(User user) {
			String deptEleName = null;//部门对应的元素名称
			if (user.getId() % 2 == 0) {
				deptEleName = "dept_2";
			} else {
				deptEleName = "dept_1";
			}
			QName deptEleQName = new QName(deptEleName);
			JAXBElement<String> deptEle = new JAXBElement<>(deptEleQName, String.class, user.getDeptName());
			return deptEle;
		}
		
	}
	
	@XmlRootElement(name="User")
	public static class User {
		private Integer id;
		private String name;
		private String deptName;
		private JAXBElement<String> dept;
		
		@XmlAttribute(name="id")
		public Integer getId() {
			return id;
		}
		
		public void setId(Integer id) {
			this.id = id;
		}
		
		@XmlElement(name="Name")
		public String getName() {
			return name;
		}
		
		public void setName(String name) {
			this.name = name;
		}
		
		@XmlTransient
		public String getDeptName() {
			return deptName;
		}
		
		public void setDeptName(String deptName) {
			this.deptName = deptName;
		}

		public JAXBElement<String> getDept() {
			return dept;
		}

		public void setDept(JAXBElement<String> dept) {
			this.dept = dept;
		}
		
	}
	

针对上面的定义,我们进行如下测试。

	@Test
	public void test2() {
		ObjectFactory objectFactory = new ObjectFactory();
		User user = new User();
		user.setId(2);
		user.setName("张三");
		user.setDeptName("部门1");
		JAXBElement<String> dept = objectFactory.createDept(user);
		user.setDept(dept);
		JAXB.marshal(user, System.out);
	}

上面的测试代码的输出结果如下:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<User id="2">
    <dept>
        <nil>false</nil>
        <value xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">部门1</value>
    </dept>
    <Name>张三</Name>
</User>

显然,这与我们预想的结果是不一样的,我们本来预想的是得到如下这样的内容。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<User id="2">
    <dept_2>部门1</dept_2>
    <Name>张三</Name>
</User>

那需要如何才能得到我们想要的结果呢?这时候需要介绍几个新的注解,@XmlRegistry@XmlElementDecl@XmlElementRef

@XmlRegistry

XmlRegistry注解用来标注在类上面,表示该类中包含有使用@XmlElementDecl标注的方法。

@XmlElementDecl

XmlElementDecl注解用来标注在方法上,该方法需要接收一个参数,且需要有返回类型。它又需要与@XmlElementRef一起使用。XmlElementDecl在使用时必须指定一个name属性,namespace属性也可以指定,以唯一的确定这样一个XmlElementDecl的声明。

@XmlElementRef

XmlElementRef注解一共有两种用法,本文只介绍其与XmlElementDecl一起使用的用法。我们可以通过XmlElementRef来指定当前的XML和对象之间的映射需要使用的是哪个XmlElementDecl指定的方法,这是通过其name属性和namespace属性来指定的,它们的值必须与对应的XmlElementDecl上声明的namenamespace一致。

采用了@XmlElementRef标注后,对应的对象转换XML形式就不会以当前的属性声明为准了,而是会以实际传递过来的值为准了。当我们的一个属性声明为JAXBElement形式,我们希望它生成的XML形式是以JAXBElement中包含的那个具体的对象为准,而不是以JAXBElement自己为准时,我们可以在该属性上使用@XmlElementRef标注,同时指向创建它的ObjectFactory的对应方法上标注的@XmlElementDecl,同时需要注意在ObjectFactory类上采用@XmlRegistry进行标注。然后在转换XML时,还需要指定使用的Class除了当前的对象外,还包括对应的ObjectFactory类。所以针对上面的需求,我们可以改为如下配置即可。

	@XmlRegistry
	public static class ObjectFactory {
		
		@XmlElementDecl(name="user.dept")
		public JAXBElement<String> createDept(User user) {
			String deptEleName = null;//部门对应的元素名称
			if (user.getId() % 2 == 0) {
				deptEleName = "dept_2";
			} else {
				deptEleName = "dept_1";
			}
			QName deptEleQName = new QName(deptEleName);
			JAXBElement<String> deptEle = new JAXBElement<>(deptEleQName, String.class, user.getDeptName());
			return deptEle;
		}
		
	}
	
	@XmlRootElement(name="User")
	public static class User {
		private Integer id;
		private String name;
		private String deptName;
		private JAXBElement<String> dept;
		
		@XmlAttribute(name="id")
		public Integer getId() {
			return id;
		}
		
		public void setId(Integer id) {
			this.id = id;
		}
		
		@XmlElement(name="Name")
		public String getName() {
			return name;
		}
		
		public void setName(String name) {
			this.name = name;
		}
		
		@XmlTransient
		public String getDeptName() {
			return deptName;
		}
		
		public void setDeptName(String deptName) {
			this.deptName = deptName;
		}

		@XmlElementRef(name="user.dept")
		public JAXBElement<String> getDept() {
			return dept;
		}

		public void setDept(JAXBElement<String> dept) {
			this.dept = dept;
		}
		
	}

在上面的代码中,我们在ObjectFactory类上加上了@XmlRegistry,在对应的createDept()方法上加上了@XmlElementDecl(name="user.dept"),同时还在UsergetDept()方法上加上了@XmlElementRef(name="user.dept")。对应的测试代码我们也调整如下,因为我们在转换XML时还需要加入ObjectFactory.class,所以我们改用JAXBContext创建Marshaller的方式进行对象和XML之间的转换。

	@Test
	public void test2() throws Exception {
		ObjectFactory objectFactory = new ObjectFactory();
		User user = new User();
		user.setId(2);
		user.setName("张三");
		user.setDeptName("部门1");
		JAXBElement<String> dept = objectFactory.createDept(user);
		user.setDept(dept);
		
		JAXBContext jaxbContext = JAXBContext.newInstance(User.class, ObjectFactory.class);
		Marshaller marshaller = jaxbContext.createMarshaller();
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		marshaller.marshal(user, System.out);
	}

应用上面的测试代码,最终的生成结果是如下这样的,这就达到了我们的期望。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<User id="2">
    <dept_2>部门1</dept_2>
    <Name>张三</Name>
</User>

上面给出的示例是基于一个普通属性的,其实对于一个复杂类型的对象也是一样的。比如我们把上面的代码改改,我们把dept改为Dept对象,我们希望生成该对象对应的XML元素时对应的元素名是Dept对象的idname通过下划线连起来的样子。具体如下所示。

	@XmlRegistry
	public static class ObjectFactory {

		@XmlElementDecl(name = "dept")
		public JAXBElement<Dept> createDept(Dept dept) {
			String deptEleName = dept.getId() + "_" + dept.getName();// 部门对应的元素名称
			QName deptEleQName = new QName(deptEleName);
			JAXBElement<Dept> deptEle = new JAXBElement<>(deptEleQName, Dept.class, dept);
			return deptEle;
		}

	}

	@XmlRootElement(name = "User")
	public static class User {
		private Integer id;
		private String name;
		private JAXBElement<Dept> dept;

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

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

		@XmlElement(name = "Name")
		public String getName() {
			return name;
		}

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

		@XmlElementRef(name = "dept")
		public JAXBElement<Dept> getDept() {
			return dept;
		}

		public void setDept(JAXBElement<Dept> dept) {
			this.dept = dept;
		}

	}

	public static class Dept {
		private int id;
		private String name;

		public int getId() {
			return id;
		}

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

		public String getName() {
			return name;
		}

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

然后把我们的测试代码也调整一下,调整成如下这样。

	@Test
	public void test2() throws Exception {
		ObjectFactory objectFactory = new ObjectFactory();
		User user = new User();
		user.setId(2);
		user.setName("张三");
		Dept dept = new Dept();
		dept.setId(1);
		dept.setName("技术部");
		JAXBElement<Dept> deptEle = objectFactory.createDept(dept);
		user.setDept(deptEle);

		JAXBContext jaxbContext = JAXBContext.newInstance(User.class, ObjectFactory.class);
		Marshaller marshaller = jaxbContext.createMarshaller();
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		marshaller.marshal(user, System.out);
	}

结果输出如下所示,也转换了。对象里面原来应该是怎么转换的还是怎么转换的,跟其它JAXB配置方式是一样的。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<User id="2">
    <1_技术部>
        <id>1</id>
        <name>技术部</name>
    </1_技术部>
    <Name>张三</Name>
</User>

需要说明的是我们定义的ObjectFactory中的方法在unmarshal时是不会调用的,进行marshal时也不会被自动调用,它的主要作用是持有@XmlRegistry和@XmlElementDecl的声明。

(本文由Elim写于2017年6月9日)