用来记录一些原创性的总结
假设我们需要生成如下这样一段XML代码,condition元素下是若干个包含一个文本节点的元素,这样的元素个数不定。如果需要把它们定义为一个Java类,很明显应该定义为Map结构比较合适。
<request>
<condition>
<key_0>value_0</key_0>
<key_1>value_1</key_1>
<key_2>value_2</key_2>
<key_3>value_3</key_3>
<key_4>value_4</key_4>
</condition>
</request>
为此,我们很容易想到的Java类结构是如下这样的。
@XmlRootElement
public class Request {
private Map<String, String> condition;
public Map<String, String> getCondition() {
return condition;
}
public void setCondition(Map<String, String> condition) {
this.condition = condition;
}
}
然后我们来做个测试,看是否可以达到我们想要的效果,测试代码如下:
@Test
public void test() throws Exception {
Request request = new Request();
Map<String, String> condition = new LinkedHashMap<>();
for (int i=0; i<5; i++) {
condition.put("key_" + i, "value_" + i);
}
request.setCondition(condition);
JAXBContext jaxbContext = JAXBContext.newInstance(Request.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(request, System.out);
}
上面的测试代码输出如下:
<request>
<condition>
<entry>
<key>key_0</key>
<value>value_0</value>
</entry>
<entry>
<key>key_1</key>
<value>value_1</value>
</entry>
<entry>
<key>key_2</key>
<value>value_2</value>
</entry>
<entry>
<key>key_3</key>
<value>value_3</value>
</entry>
<entry>
<key>key_4</key>
<value>value_4</value>
</entry>
</condition>
</request>
很明显这没有达到我们想要的效果。不像集合类型那样,JAXB没有对Map进行特殊的处理,它纯粹把它当做一个普通的Java对象来解析的,所以出来的效果就是上面那样的。既然普通的Map会按照Java对象来解析,那么我们如果想要Map结构按照预想的那样转换为XML,那么我们是否可以构造出类似的结构呢?按照这种想法,笔者的思路是把我们的Map包装起来,应用XmlElementWrapper的特殊处理,自定义一个继承自ArrayList的类,把每一个key/value都当做是ArrayList中的一个元素。由于这个元素在展示为XML时需要获取动态的元素名(取key的名称),为此,把它封装为一个JAXBElement对象。为此笔者自己创建的简单的具有类似Map功能的XmlMap结构如下:
public class XmlMap extends ArrayList<JAXBElement<String>> {
/**
*
*/
private static final long serialVersionUID = -7047805724967522158L;
private Map<String, JAXBElement<String>> keyMap = new HashMap<>();
private static ObjectFactory objectFactory = new ObjectFactory();
public void put(String key, String value) {
if (keyMap.containsKey(key)) {
super.remove(keyMap.get(key));
}
JAXBElement<String> ele = objectFactory.createXmlMap(key, value);
super.add(ele);
keyMap.put(key, ele);
}
}
对应的ObjectFactory类定义如下:
@XmlRegistry
public class ObjectFactory {
@XmlElementDecl(name = "xmlMap")
public JAXBElement<String> createXmlMap(String key, String value) {
QName name = new QName(key);
JAXBElement<String> ele = new JAXBElement<>(name, String.class, value);
return ele;
}
}
ObjectFactory中的createXmlMap把一个key/value对封装为了一个JAXBElement对象,且对应的XML元素名称是取的key的值。然后把我们的Request中的Map替换为我们自定义的XmlMap。此时Request中的getCondition()也需要通过@XmlElementRef(name=”xmlMap”)关联ObjectFactory中定义的xmlMap。
@XmlRootElement
public class Request {
private XmlMap condition;
public void setCondition(XmlMap condition) {
this.condition = condition;
}
@XmlElementWrapper(name = "condition")
@XmlElementRef(name = "xmlMap")
public XmlMap getCondition() {
return this.condition;
}
}
测试类里面其它的代码都不需要变化,只需要把Map替换为XmlMap,并在创建JAXBContext时加入应用到的ObjectFactory.class。
@Test
public void test() throws Exception {
Request request = new Request();
XmlMap condition = new XmlMap();
for (int i = 0; i < 5; i++) {
condition.put("key_" + i, "value_" + i);
}
request.setCondition(condition);
JAXBContext jaxbContext = JAXBContext.newInstance(Request.class, ObjectFactory.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(request, System.out);
}
然后运行上面的测试用例,出来的效果如下:
<request>
<condition>
<key_0>value_0</key_0>
<key_1>value_1</key_1>
<key_2>value_2</key_2>
<key_3>value_3</key_3>
<key_4>value_4</key_4>
</condition>
</request>
跟我们一开始期望的一样。笔者示例中的XmlMap只有简单的类似于java.util.Map的存储单个元素的功能,如需要更多的类似Map的功能,则可以做进一步的封装。接下来,笔者还会介绍另一种把Map转换为XML的方法,其在Request中定义的将是一个真正的java.util.Map对象。
在方法一中,我们使用了一个自定义的XmlMap类,用以封装类似于Map的结构以存储数据,然后把它转换为XML。但是它的结构比较简单,如果我们需要使用真实的Map的话,我们需要把我们的XmlMap实现java.util.Map接口,然后实现其中的各个方法并把它们转换为ArrayList中的一个元素。这显然是比较麻烦的。为此笔者提供了一种把真实的Map转换为XML的方法,而不是直接使用自定义的XmlMap。大体思路是定义一个XmlAdapter,把一个真实类型的Map转换为XmlMap,然后在转换为XML时还是根据XmlMap来进行转换,这对它的使用者来讲会更加的友好。使用者不用关心是怎么转换的,只需要按照正常的逻辑使用Map结构即可。
定义了一个MapAdapter,其继承自XmlAdapter,用来将java.util.Map转换为我们自定义的XmlMap,其实现如下:
public class MapAdapter extends XmlAdapter<XmlMap, Map<String, String>> {
@Override
public Map<String, String> unmarshal(XmlMap v) throws Exception {
return null;
}
@Override
public XmlMap marshal(Map<String, String> v) throws Exception {
if (v != null) {
XmlMap xmlMap = new XmlMap();
for (Map.Entry<String, String> entry : v.entrySet()) {
xmlMap.put(entry.getKey(), entry.getValue());
}
return xmlMap;
}
return null;
}
}
然后XmlMap的结构也要改一改,不再继承自ArrayList,而是直接持有一个List类型的属性,因为使用了XMLAdapter后,XmlMap会被直接当做一个对象进行转换,而不是像方法一那样被当做一个集合来进行转换。因此为了确保其能够正确的输出key/value形式的元素,我们将XmlMap改造为如下这样,输出时实际将输出elements中的内容。
@XmlAccessorType(XmlAccessType.FIELD)
public class XmlMap {
@XmlElementRef(name = "xmlMap")
private List<JAXBElement<String>> elements = new ArrayList<>();
private static ObjectFactory objectFactory = new ObjectFactory();
public void put(String key, String value) {
JAXBElement<String> ele = objectFactory.createXmlMap(key, value);
this.elements.add(ele);
}
}
ObjectFactory的内容还是不变。然后Request中的condition定义为java.util.Map类型。
@XmlRootElement
public static class Request {
private Map<String, String> condition;
@XmlJavaTypeAdapter(MapAdapter.class)
public Map<String, String> getCondition() {
return condition;
}
public void setCondition(Map<String, String> condition) {
this.condition = condition;
}
}
测试代码如下,在测试代码中我们应用了LinkedHashMap,这是为了使输出的XML能够按照存入的顺序输出。
@Test
public void test() throws Exception {
Request request = new Request();
Map<String, String> condition = new LinkedHashMap<>();
for (int i = 0; i < 5; i++) {
condition.put("key_" + i, "value_" + i);
}
request.setCondition(condition);
JAXBContext jaxbContext = JAXBContext.newInstance(Request.class, ObjectFactory.class);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(request, System.out);
}
输出结果如下:
<request>
<condition>
<key_0>value_0</key_0>
<key_1>value_1</key_1>
<key_2>value_2</key_2>
<key_3>value_3</key_3>
<key_4>value_4</key_4>
</condition>
</request>
也正常的按照我们想要的Map结构进行了输出。
接下来将介绍两种XML转Map的方式,这两种方式都可以兼容Map转XML,区别在于一种是针对于Map在XML中有根节点,一种是没有根节点的。
考虑下面这样一段XML,root根节点下有other节点和map节点,这两个是固定的,但是map节点下的节点的数量和名称是不固定的,形式都是<key>value</key>
式的。
<root>
<other>123</other>
<map>
<key1>value1</key1>
<key2>value2</key2>
<key3></key3>
<key4 />
</map>
</root>
很明显,这样的XML映射为Java中的Map比较合适,为此定义了与它匹配的Java类的结构如下:
@XmlRootElement(name="root")
public class RootObj {
private String other;
private Map<String, String> map;
public String getOther() {
return other;
}
public void setOther(String other) {
this.other = other;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
很不幸的是JAXB不支持直接这样映射,它能支持的是把map节点下的动态节点用XmlAnyElement进行映射,而map节点可以映射为一个对象,为此建立了一个能够接收map节点及其以下字节点类来作为中转,其结构如下:
@XmlAccessorType(XmlAccessType.FIELD)
public class MapHolder {
@XmlAnyElement
private List<Element> entries;
public List<Element> getEntries() {
return entries;
}
public void setEntries(List<Element> entries) {
this.entries = entries;
}
}
刚刚说了MapHolder是用于作为中转的,而我们真正最终要映射的是Map<String, String>,为此建立了一个对应的适配器。使用JAXB的XmlAdapter机制,其实现如下。其中unmarshal用于XML转Map,marshal用于Map转XML。
public class XmlToMapAdapter extends XmlAdapter<MapHolder, Map<String, String>> {
@Override
public Map<String, String> unmarshal(MapHolder v) throws Exception {
if (v == null) {
return null;
}
Map<String, String> map = new HashMap<>();
List<Element> entries = v.getEntries();
if (entries != null && !entries.isEmpty()) {
entries.forEach(ele -> {
String key = ele.getLocalName();
String value = ele.getTextContent();
map.put(key, value);
});
}
return map;
}
@Override
public MapHolder marshal(Map<String, String> v) throws Exception {
if (v == null) {
return null;
}
MapHolder holder = new MapHolder();
if (!v.isEmpty()) {
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
List<Element> entries = new ArrayList<>();
v.forEach((key, value) -> {
Element ele = doc.createElement(key);
ele.setTextContent(value);
entries.add(ele);
});
holder.setEntries(entries);
}
return holder;
}
}
定义了Map到MapHolder的适配器XmlToMapAdapter后,我们需要告诉JAXB,为此我们在RootObj的map属性上加上@XmlJavaTypeAdapter指定使用的XmlAdapter是XmlToMapAdapter,代码如下:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name="root")
public static class RootObj {
private String other;
@XmlJavaTypeAdapter(XmlToMapAdapter.class)
private Map<String, String> map;
public String getOther() {
return other;
}
public void setOther(String other) {
this.other = other;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
至此,我们Map结构的XML就能够顺利的转换为Map了,而Map也可以顺利的转换为XML格式的Map了。进行单元测试如下:
@Test
public void test() throws Exception {
String xml = "<root><other>123</other><map><key1>value1</key1><key2>value2</key2><key3></key3><key4/></map></root>";
RootObj rootObj = this.unmarshal(xml);//XML转对象
JAXBContext jaxbContext = JAXBContext.newInstance(RootObj.class);
Marshaller marshaller = jaxbContext.createMarshaller();
StringWriter writer = new StringWriter();
marshaller.marshal(rootObj, writer);//对象转XML
this.unmarshal(writer.toString());//验证marshal的结果是否正确
}
private RootObj unmarshal(String xml) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(RootObj.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
RootObj rootObj = (RootObj) unmarshaller.unmarshal(new StringReader(xml));
Assert.assertNotNull(rootObj.getMap());
Assert.assertEquals("123", rootObj.getOther());
Assert.assertEquals(4, rootObj.getMap().size());
Assert.assertEquals("value1", rootObj.getMap().get("key1"));
Assert.assertEquals("value2", rootObj.getMap().get("key2"));
Assert.assertEquals("", rootObj.getMap().get("key3"));//key3的值是空字符串
Assert.assertEquals("", rootObj.getMap().get("key4"));//key4的值是空字符串
return rootObj;
}
上面的单元测试应用了断言,自动化的方式,单元测试能够顺利跑完说明我们能够把那段XML转换为需要的Map结构,也能把Map结构转换为对应的XML结构。如果你不放心上面的结果,想看看具体转换出来的内容,可以把上面的代码改改,输出对应的转换结果到控制台。
本文将介绍Map中的Key元素的父节点下的子节点除了Map中的Key元素外,还拥有其它固定的节点,或者更有甚者是这些节点是直接定义在根节点下面的。考虑下面这样一段XML,root是根节点,根节点下的other节点是一个固定的节点,而key1、key2、key3则是动态的,不一定有,名称也不固定。
<root>
<key1>value1</key1>
<key2>value2</key2>
<key3 />
<other>123</other>
</root>
根据这样的结构定义了一个与它映射的Java类,其中的动态部分采用Map映射,而不是用List
@XmlRootElement(name="root")
@XmlAccessorType(XmlAccessType.FIELD)
public class RootObj {
private String other;
@XmlJavaTypeAdapter(XmlToMapAdapter.class)
@XmlAnyElement
private Map<String, String> map;
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
我们的Map是用来直接映射root节点下的动态节点的,所以采用XmlAnyElement注解标注。XmlAnyElement能映射的是Element,而我们的目标是Map,所以我们定义了一个XmlToMapAdapter这样的适配器,通过XmlJavaTypeAdapter指定。
/**
* XmlAnyElement映射的是单个的Element对象,把它们转成Map时需要自己维护好所属的Map对象,以便共用
*/
public class XmlToMapAdapter extends XmlAdapter<Element, Map<String, String>> {
private static ThreadLocal<Map<String, String>> mapHolder = new ThreadLocal<Map<String, String>>() {
@Override
protected Map<String, String> initialValue() {
return new HashMap<>();
}
};
/**
* 清除线程变量
*/
public static void clear() {
mapHolder.remove();
}
@Override
public Map<String, String> unmarshal(Element v) throws Exception {
if (v == null) {
return null;
}
Map<String, String> map = mapHolder.get();
String key = v.getLocalName();
String value = v.getTextContent();
map.put(key, value);
return map;
}
@Override
public Element marshal(Map<String, String> v) throws Exception {
//这种场景就不是很好处理了,因为返回的只有一个Element,而我们期望的是返回多个平级的节点
//这时候的一种替代方案是在同级的类中定义一个List<JAXBElement<String>>类型的属性作为
//marshal时Map的替代,而Map的marshal此时将返回null
return null;
}
}
该适配器中转换Element到Map时每一个动态节点都会触发一次转换,而转换后的Map对象都是赋值给了RootObj的map属性,如果不加以处理,map中永远只包含最后一个动态节点。为此我们需要把Map保存起来,笔者的示例中应用了一个线程变量,这是假设一次转换过程中只会有一种需映射为Map的场景,如果有多种适合通过监听器辅助线程变量来实现类似的功能。这里用一个线程变量存储起对应的Map,保证该Map中线程内共享,这样每一个动态节点都可以添加到同一个Map中了,最后返回的也是一个完整的Map。unmarshal之后需要调用XmlToMapAdapter的clear()以便清除线程变量。这样我们的示例中的XML映射为Map的需求就完成了,但是以这样的定义映射为XML就不行了。因为我们的XmlToMapAdapter中的marshal需要把一个Map转换为一个Element,Map中可能有N个Key/Value组合,但是最终却只能产生一个Element。这一个Element对应于XML中就是一个节点而已。那怎么办呢?这个时候可以配合Marshal监听器一起使用,笔者这里简单起见就用基于实例的监听器,实际应用中基于全局的监听器加上一些封装会更通用。笔者的思路是在RootObj中新增一个用来在marshal时替换Map的属性,marshal时就用该属性来生成对应的XML。能满足此需求的是List
@XmlRootElement(name="root")
@XmlAccessorType(XmlAccessType.FIELD)
public class RootObj {
private String other;
@XmlJavaTypeAdapter(XmlToMapAdapter.class)
@XmlAnyElement
private Map<String, String> map;
@XmlElementRef(name="xmlMapEle")
private List<JAXBElement<String>> mapReplacement;
public void beforeMarshal(Marshaller marshaller) {
if (this.map == null || this.map.isEmpty()) {
return;
}
mapReplacement = new ArrayList<>(this.map.size());
map.forEach((key, value) -> {
QName name = new QName(key);
JAXBElement<String> ele = new JAXBElement<>(name, String.class, value);
mapReplacement.add(ele);
});
}
public String getOther() {
return other;
}
public void setOther(String other) {
this.other = other;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
}
在上面的代码中,mapReplacement在映射为XML时不能使用默认的mapReplacement作为节点名,也不能通过XmlElement指定一个固定的节点名,它们必须是动态的,由JAXBElement对象自己决定的,为此增加了@XmlElementRef(name=”xmlMapEle”),以及对应的ObjectFactory,用以声明名为xmlMapEle的元素的定义。ObjectFactory的代码如下所示:
@XmlRegistry
public static class ObjectFactory {
@XmlElementDecl(name="xmlMapEle")
public JAXBElement<String> createXmlMapEle(String key, String value) {
QName name = new QName(key);
JAXBElement<String> ele = new JAXBElement<>(name, String.class, value);
return ele;
}
}
至此就可以解决前面那段XML与Java的marshal和unmarshal问题了,单元测试如下:
@Test
public void test() throws Exception {
String xml = "<root><key1>value1</key1><key2>value2</key2><key3/><other>123</other></root>";
RootObj rootObj = this.unmarshal(xml);
JAXBContext jaxbContext = JAXBContext.newInstance(RootObj.class, ObjectFactory.class);
Marshaller marshaller = jaxbContext.createMarshaller();
StringWriter writer = new StringWriter();
marshaller.marshal(rootObj, writer);
this.unmarshal(writer.toString());//验证marshal的结果是否正确
}
private RootObj unmarshal(String xml) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(RootObj.class, ObjectFactory.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
RootObj rootObj = (RootObj) unmarshaller.unmarshal(new StringReader(xml));
XmlToMapAdapter.clear();
Assert.assertNotNull(rootObj.getMap());
Assert.assertEquals("123", rootObj.getOther());
Assert.assertEquals(3, rootObj.getMap().size());
Assert.assertEquals("value1", rootObj.getMap().get("key1"));
Assert.assertEquals("value2", rootObj.getMap().get("key2"));
Assert.assertEquals("", rootObj.getMap().get("key3"));//key3的值是空字符串
return rootObj;
}
(完)