JAXB-5 动态 XML 生成
5 动态 XML 生成
JAXB 通过 JAXB.marshal 和 JAXB.unmarshal 接口,根据对象树的注解(Annotation),与 XML 文档相互转换;这类转换,通过 Java 类和注解(Annotation)的协调,完成 XML 的转换,我们称之为静态 XML 转换。更进一步地,通过 JAXBContext 相关接口,在 XML 转换过程中,进行 XML 结构性调整的方式,称之为动态 XML 转换。
5.1 实现 XML 动态 Element
使用 List<Object>
+ @XmlAnyElement
实现 XML 动态 Element
有时候,Products元素下面并不只是加入 product ,可能动态加入这种商品。 这次的模拟的商品是糕点与饼干。
这里的 Cake 只包含一个字段,需要注意的是 @XmlRootElement不能少。
@XmlRootElement(name = "Cake")
public class Cake {
private String name;
//setters, getters
}
这里的 Biscuit 也只包含一个字段。
@XmlRootElement(name = "Biscuit")
public class Biscuit {
private String name;
//setters, getters
}
Order的第三个字段是List,但是没有指定一个特定对象,用了Object代指所有,还有一个 @XmlAnyElement是最重要的注解,用来标注所有的Element,用于解组时 catch-all 捕获所有未能解组的元素。
@XmlRootElement(name = "Order")
@XmlAccessorType(XmlAccessType.FIELD)
public class Order4 {
private String id;
private Double price;
@XmlElementWrapper(name = "Products")
@XmlAnyElement
private List<Object> product;
//setters, getters
}
下面用来模拟数据生成,注意的是JAXBContext需要注册所有需要编组的Java bean。这一点和之前的例子是不同的。
@Test
public void test4() throws JAXBException {
Cake cake = new Cake();
cake.setName("Nobel");
Biscuit biscuit = new Biscuit();
biscuit.setName("PB");
List<Object> list = Arrays.asList(cake, biscuit);
Order4 order = new Order4();
order.setId("1104");
order.setProduct(list);
JAXBContext context = JAXBContext.newInstance(Order4.class, Cake.class, Biscuit.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(order, System.out);
}
生成的XML包含了一个 Products ,其数据结构在Order中并没有指定,方便后期扩展。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Order>
<id>1104</id>
<Products>
<Cake>
<name>Nobel</name>
</Cake>
<Biscuit>
<name>PB</name>
</Biscuit>
</Products>
</Order>
5.2 实现 XML 动态 Attribute
使用 Map<QName, String>
+ @XmlAnyAttribute
实现 XML 动态 Attribute
与动态Element对应的是Attribute,不过需要注意的是,动态 Attribute 需要的是 Map ,而且其 key 的类型需要指定为 QName,这个QName在复杂的 XML 生成时有很大用处。一般使用到的是其QName(name),它还有一个形式为QName(name,namespace),可以指定命名空间,在某些场景下有不可替代的作用。
注意,需要为该动态 Element 标明 @XmlAnyAttribute 注解,用于 catch-all 所有未能解析的属性。
@XmlRootElement(name = "Order")
@XmlAccessorType(XmlAccessType.FIELD)
public class Order5 {
@XmlAnyAttribute
private Map<QName, String> properties;
private Product product;
//setters, getters
}
@Test
public void test5() throws JAXBException {
Product p = new Product();
p.setId("1100");
p.setName("Apple");
Map<QName, String> map = new HashMap<>();
map.put(new QName("id"), "1105");
map.put(new QName("classification"), "food");
map.put(new QName("type"), "fruit");
Order5 order = new Order5();
order.setProduct(p);
order.setProperties(map);
JAXBContext context = JAXBContext.newInstance(Order5.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(order, System.out);
}
仅关注 Order 的 Attribute , 可以发现我们在 Order 中没有指定任何与之相关的字段,只是在HashMap中加了几组数据,现在编组成为了Attribute。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Order id="1105" classification="food" type="fruit">
<product id="1100">
<name>Apple</name>
</product>
</Order>
5.3 动态 XML 节点
使用 JAXBElement 封装对象,变更转换后的 XML 节点名称。
5.3.1 动态根节点
动态根节点
已知:如果 @XmlRootElement不指定参数,则使用类名首字母小写作为根节点,如果指定name参数则使用其值作为根节点。
场景假设:XML的根节点需要根据业务场景变化,上例中的<水果>可以是任何传入的值,那么现有的方案无法实现这样的场景。
解决办法:使用 JAXBElement 类封装 Java Bean 类对象,实现与 @XmlRootElement 同样的效果;JAXBElement,它可以代指任意 XML Element,并且在其初始化时,需要指定几个重要参数。
@Test
public void test2() throws JAXBException {
Fruit fruit = new Fruit();
fruit.setColor("red");
JAXBElement<Fruit> element = new JAXBElement<Fruit>(new QName("新鲜水果"), Fruit.class, fruit);
JAXB.marshal(element, System.out);
}
和上例的不同点在于编组(mashall)的是 JAXBElement,而不直接作用于 Fruit,其第一个参数 QName就是指定根节点的名字,第二个参数指定需要编组的对象,第三个参数是真正的数据。要注意最后一行代码,传入的参数是 element。
得到的结果:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<新鲜水果>
<color>red</color>
</新鲜水果>
5.3.2 动态子节点
动态子节点
既然使用 JAXBElement 可以动态指定参数值,如果某个Java 字段使用该类型是否可以做到动态生成XML子节点呢: Yes & No。
定义一个零食,第二个参数是 JAXBElement 的 水果,Fruit在之前一定定义过了。
@XmlRootElement
public class Food {
private String name;
private JAXBElement<Fruit> element;
// setters,getters
}
这里还需要指定一个 ObjectFactory,ObjectFactory 类型的类里面可以定义一些创建某种类型的对象的方法,@XmlRegistry 用于标注在充当 ObjectFactory 角色的类上,@XmlElementDecl 声明对应的元素定义,其方法的返回值需要是JAXBElement类型,并且它必须指定一个name,这个name自由赋值,这里指定为’ref1’备用。
customElement 方法我直接返回null,因为实现细节不需要在这里写死,等下创建对象的时候再声明。
@XmlRegistry
public class ObjectFactory {
@XmlElementDecl(name = "ref1")
public JAXBElement<Fruit> customElement(Fruit fruit){
return null;
}
}
在Food中定义了 JAXBElement,需要使用 @XmlElementRef(name=“ref1”)关联使用到了 ObjectFactory 哪个方法,可以把@XmlElementRef(name=“ref1”)标注在对应的setter/getter方法上,或者标注在字段上,不过需要注意的是标注在字段上,还需要指定@XmlAccessorType(XmlAccessType.FIELD).
如果加在get方法上就不需要加@XmlAccessorType.
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Food {
private String name;
@XmlElementRef(name="ref1")
private JAXBElement<Fruit> element;
// setters,getters
// @XmlElementRef(name="ref1")
public JAXBElement<Fruit> getElement() {
return element;
}
}
测试一下上面的写法是否正确。
@Test
public void test4() throws JAXBException {
Fruit fruit = new Fruit();
fruit.setColor("red");
JAXBElement<Fruit> element = new JAXBElement<Fruit>(new QName("时令水果"), Fruit.class, fruit);
Food food = new Food();
food.setName("Some foods");
food.setElement(element);
JAXBContext context = JAXBContext.newInstance(Fruit.class,Food.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(food, System.out);
}
可以看到XML的子节点Fruit并不是之前指定的@XmlRootElement,而是测试代码中设置的值。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<food>
<name>Some foods</name>
<时令水果>
<color>red</color>
</时令水果>
</food>
更改QName的值为‘生鲜水果’,发现生成的XML跟着变化。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<food>
<name>Some foods</name>
<生鲜水果>
<color>red</color>
</生鲜水果>
</food>
5.3.3 利用继承关系
利用继承关系
已知:XML 中的节点元素都是对应着 Java 类的实例;
方案:可以利用继承关系,创建子类的实例,生成子类的 XML 节点元素,来动态生成 XML 元素。
‘商品信息’(Product.java)是之前用过的例子,它的第二个字段是引用类型:
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Product {
@XmlAttribute
private String id;
@XmlElementRef
private Fruit fruit;
// setters,getters
}
‘水果’(Fruit.java)只有一个字段,并且已经设置了别名@XmlRootElement(name = “水果”):
@XmlRootElement(name = "水果")
public class Fruit {
private String color;
// setters,getters
}
‘水果1’()继承了‘水果’,并且有一个特殊字段:
@XmlRootElement
public class Pomelo extends Fruit{
private String name;
// setters,getters
}
‘水果2’()继承了‘水果’,并且有一个特殊字段:
@XmlRootElement
public class Watermelon extends Fruit{
private String shape;
// setters,getters
}
当商品信息是第一种水果时:
@Test
public void test5() throws JAXBException {
Pomelo pomelo = new Pomelo();
pomelo.setName("柚子");
pomelo.setColor("Orange");
Product product = new Product();
product.setFruit(pomelo);
product.setId("1205");
JAXBContext context = JAXBContext.newInstance(Product.class,
Pomelo.class,
Fruit.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(product, System.out);
}
生成的 XML 如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<product id="1205">
<pomelo>
<color>Orange</color>
<name>柚子</name>
</pomelo>
</product>
换一种水果再看看:
@Test
public void test5_2() throws JAXBException {
Watermelon watermelon = new Watermelon();
watermelon.setShape("椭圆形");
watermelon.setColor("Green");
Product product = new Product();
product.setFruit(watermelon);
product.setId("1205");
JAXBContext context = JAXBContext.newInstance(Product.class,
Watermelon.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(product, System.out);
}
生成的 XML 如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<product id="1205">
<watermelon>
<color>Green</color>
<shape>椭圆形</shape>
</watermelon>
</product>
商品信息每次根据不同的子商品而变化,之前已经设置过的主商品Fruit已经不能影响最终结果。
需要注意的是,这里不能直接使用静态工具类JAXB,下面的方式生成的结果不正确:
@Test
public void test5_3() throws JAXBException {
Watermelon watermelon = new Watermelon();
watermelon.setShape("椭圆形");
watermelon.setColor("Green");
Product product = new Product();
product.setFruit(watermelon);
product.setId("1205");
JAXB.marshal(product, System.out);
}
得到的 XML 和之前的预期不一致:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<product id="1205">
<水果>
<color>Green</color>
</水果>
</product>
因为 JAXB 工具类在注册newInstance时,只关注第一个参数JAXB.marshal(object, out),而这里的第一个参数是Product,因此不能注册Fruit的子类 Watermelon,所有与 Watermelon 相关的设置都不能成功,不过这里与父类 Fruit 相关的设置都生效了。
上一章:JAXB-4 Java 类与 XML 的转换
目录:学习 JAXB
下一章:JAXB-6 XML 应用案例