Elim的博客

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


Elim

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

在上一篇介绍了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。为此新增一个替换属性叫mapReplacement,List<JAXBElement>类型。新增实例级别的marshal监听方法beforeMarshal,在该方法中实现Map<String, String>到List<JAXBElement>的转换。调整后的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);
        });
    }
    
}

在上面的代码中,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系列中的一篇,如果单纯看这篇看不懂的,请按照博客系列顺序进行查看,以掌握必要的基础知识。