Elim的博客

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


Elim

XML与Map相互转化

Map转XML

方法一

假设我们需要生成如下这样一段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

接下来将介绍两种XML转Map的方式,这两种方式都可以兼容Map转XML,区别在于一种是针对于Map在XML中有根节点,一种是没有根节点的。

有Map根节点

考虑下面这样一段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根节点

本文将介绍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。为此新增一个替换属性叫mapReplacement,List<JAXBElement>类型。新增实例级别的marshal监听方法beforeMarshal,在该方法中实现Map<String, String>到List<JAXBElement>的转换。调整后的RootObj类定义如下:

@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;
}

(完)