Elim的博客

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


Elim

JAXB系列之XML转Map之有Map根节点

之前介绍过两种Map转XML的方式,接下来将介绍两种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类的结构如下:

@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name="root")
public static class RootObj {
    private String other;
    private Map<String, String> map;
}

很不幸的是JAXB不支持直接这样映射,它能支持的是把map节点下的动态节点用XmlAnyElement进行映射,而map节点可以映射为一个对象,为此建立了一个能够接收map节点及其以下字节点类来作为中转,其结构如下:

@Data
@XmlAccessorType(XmlAccessType.FIELD)
public static class MapHolder {
    @XmlAnyElement
    private List<Element> entries;
}

刚刚说了MapHolder是用于作为中转的,而我们真正最终要映射的是Map<String, String>,为此建立了一个对应的适配器。使用JAXB的XmlAdapter机制,其实现如下。其中unmarshal用于XML转Map,marshal用于Map转XML。

public static 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,代码如下:

@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name="root")
public static class RootObj {
    private String other;
    @XmlJavaTypeAdapter(XmlToMapAdapter.class)
    private Map<String, String> 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);
    JAXBContext jaxbContext = JAXBContext.newInstance(RootObj.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);
    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;
}

完整代码如下:

import java.io.StringReader;
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.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.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.parsers.DocumentBuilderFactory;

import org.junit.Assert;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import lombok.Data;

/**
 * 这种介绍的是用一个元素包裹起来的Map,即Map是有根节点的
 * @author Elim
 */
public class XmlToMapTest {

    @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);
        JAXBContext jaxbContext = JAXBContext.newInstance(RootObj.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);
        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;
    }
    
    @Data
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name="root")
    public static class RootObj {
        private String other;
        @XmlJavaTypeAdapter(XmlToMapAdapter.class)
        private Map<String, String> map;
    }
    
    @Data
    @XmlAccessorType(XmlAccessType.FIELD)
    public static class MapHolder {
        @XmlAnyElement
        private List<Element> entries;
    }
    
    public static 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;
        }
        
    }
    
}

注:本文是由Elim所写的JAXB系列中的一篇,如果单纯看这篇看不懂的,请按照博客系列顺序进行查看,以掌握必要的基础知识。