用来记录一些原创性的总结
在上一篇介绍了XML转Map时,Map中的Key元素是被一个节点包起来的,本文将介绍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
@Data
@XmlRootElement(name="root")
@XmlAccessorType(XmlAccessType.FIELD)
public static class RootObj {
private String other;
@XmlJavaTypeAdapter(XmlToMapAdapter.class)
@XmlAnyElement
private Map<String, String> map;
}
我们的Map是用来直接映射root节点下的动态节点的,所以采用XmlAnyElement注解标注。XmlAnyElement能映射的是Element,而我们的目标是Map,所以我们定义了一个XmlToMapAdapter这样的适配器,通过XmlJavaTypeAdapter指定。
/**
* XmlAnyElement映射的是单个的Element对象,把它们转成Map时需要自己维护好所属的Map对象,以便共用
* @author Elim
*/
public static 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
@Data
@XmlRootElement(name="root")
@XmlAccessorType(XmlAccessType.FIELD)
public static 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);
});
}
}
在上面的代码中,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;
}
完整代码如下:
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElementDecl;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlRegistry;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.namespace.QName;
import org.junit.Assert;
import org.junit.Test;
import org.w3c.dom.Element;
import lombok.Data;
/**
* 这种介绍的是Map没有直接被一个根节点包裹起来的情况,
* 即Map元素对应的根节点下的元素并不是都属于该Map
* @author Elim
*/
public class XmlToMapTest2 {
@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;
}
@Data
@XmlRootElement(name="root")
@XmlAccessorType(XmlAccessType.FIELD)
public static 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);
});
}
}
/**
* XmlAnyElement映射的是单个的Element对象,把它们转成Map时需要自己维护好所属的Map对象,以便共用
* @author Elim
*/
public static 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;
}
}
@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;
}
}
}
注:本文是由Elim所写的JAXB系列中的一篇,如果单纯看这篇看不懂的,请按照博客系列顺序进行查看,以掌握必要的基础知识。