Java JAXB的通用映射

Java JAXB的通用映射,java,jaxb,Java,Jaxb,好的,所以我们都知道映射在JAXB中有点麻烦。 我在这里提出了当前解决方案的替代方案。我的主要目标是获得有关此解决方案的任何和所有潜在问题的反馈。也许出于某些原因,这甚至不是一个好的解决方案 当我使用标准时,似乎没有使用类的适配器。而是扫描类,迫使我用JAXB注释标记数据模型,并在不需要的地方添加默认构造函数(我说的是要存储在映射中的复杂类,而不是简单的数据类型)。最重要的是,这使我的内部数据模型公开,从而打破了封装,因为生成的XML是内部结构的直接表示 我所做的“变通方法”是将适配器与Mars

好的,所以我们都知道映射在JAXB中有点麻烦。 我在这里提出了当前解决方案的替代方案。我的主要目标是获得有关此解决方案的任何和所有潜在问题的反馈。也许出于某些原因,这甚至不是一个好的解决方案

当我使用标准时,似乎没有使用类的适配器。而是扫描类,迫使我用JAXB注释标记数据模型,并在不需要的地方添加默认构造函数(我说的是要存储在映射中的复杂类,而不是简单的数据类型)。最重要的是,这使我的内部数据模型公开,从而打破了封装,因为生成的XML是内部结构的直接表示

我所做的“变通方法”是将适配器与
Marshall.Listener
Unmarshall.Listner
结合起来,从而能够提取额外的注释信息。然后,将创建一个字段

@XmlElement(name = "testMap")
@XmlJavaTypeAdapter(MapAdapter.class)
@MapKeyValueAdapters(key=SomeComplexClassAdapter.class)
private final HashMap<SomeComplexClass, String> testMap2 = new HashMap<SomeComplexClass, String>();
这里是适配器

import java.io.StringReader;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBIntrospector;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.namespace.QName;

/**
 * This class represents a general purpose Map adapter. It is capable of handling any type of class implementing the Map
 * interface and has a no-args constructor.
 */
public class MapAdapter extends XmlAdapter<MapAdapter.Wrapper, Map<Object, Object>> {

  private static final String XSI_NS = "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
  private static final String XSI_TYPE = "xsi:type";

  private static final String CDATA_START = "<![CDATA[";
  private static final String CDATA_END = "]]>";

  private final MarshallerListener marshallerListener = new MarshallerListener();
  private final UnmarshallerListener unmarshallerListener = new UnmarshallerListener();

  private final JAXBContext context;

  public MapAdapter(JAXBContext inContext) {
    context = inContext;
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  @Override
  public Map<Object, Object> unmarshal(Wrapper inWrapper) throws Exception {
    if (inWrapper == null) {
      return null;
    }
    Info info = null;
    for (Info element : unmarshallerListener.infoList) {
      if (element.field.equals(inWrapper.field)) {
        info = element;
      }
    }
    if (info != null) {
      Class<Map<Object, Object>> clazz = (Class<Map<Object, Object>>) Class.forName(inWrapper.mapClass);
      Map<Object, Object> outMap = clazz.newInstance();
      XmlAdapter<Object, Object> keyAdapter = null;
      XmlAdapter<Object, Object> valueAdapter = null;
      if (info.adapters.key() != MapKeyValueAdapters.UNDEFINED.class) {
        keyAdapter = (XmlAdapter<Object, Object>) info.adapters.key().getConstructor().newInstance();
      }
      if (info.adapters.value() != MapKeyValueAdapters.UNDEFINED.class) {
        valueAdapter = (XmlAdapter<Object, Object>) info.adapters.value().getConstructor().newInstance();
      }
      Unmarshaller um = context.createUnmarshaller();
      for (MapEntry entry : inWrapper.mapList) {
        Object key = ((JAXBElement) um.unmarshal(new StringReader(entry.key))).getValue();
        if (keyAdapter != null) {
          key = keyAdapter.unmarshal(key);
        }
        Object value = ((JAXBElement) um.unmarshal(new StringReader(entry.value))).getValue();
        if (valueAdapter != null) {
          value = valueAdapter.unmarshal(value);
        }
        outMap.put(key, value);
      }
      return outMap;
    } else {
      throw new IllegalStateException("Adapter info could not be found.");
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public Wrapper marshal(Map<Object, Object> inMap) throws Exception {
    if (inMap == null) {
      return null;
    }

    Info info = null;
    for (Info element : marshallerListener.infoList) {
      if (element.map == inMap) {
        info = element;
      }
    }
    if (info != null) {
      Wrapper outWrapper = new Wrapper();
      outWrapper.mapClass = inMap.getClass().getName();
      outWrapper.field = info.field;
      Marshaller m = context.createMarshaller();
      m.setProperty(Marshaller.JAXB_FRAGMENT, true);
      JAXBIntrospector introspector = context.createJAXBIntrospector();
      XmlAdapter<Object, Object> keyAdapter = null;
      XmlAdapter<Object, Object> valueAdapter = null;
      if (info.adapters.key() != MapKeyValueAdapters.UNDEFINED.class) {
        keyAdapter = (XmlAdapter<Object, Object>) info.adapters.key().getConstructor().newInstance();
      }
      if (info.adapters.value() != MapKeyValueAdapters.UNDEFINED.class) {
        valueAdapter = (XmlAdapter<Object, Object>) info.adapters.value().getConstructor().newInstance();
      }
      for (Map.Entry<?, ?> entry : inMap.entrySet()) {
        MapEntry jaxbEntry = new MapEntry();
        outWrapper.mapList.add(jaxbEntry);
        Object key = entry.getKey();
        if (key != null) {
          Class<Object> clazz = Object.class;
          if (keyAdapter != null) {
            key = keyAdapter.marshal(key);
            clazz = (Class<Object>) key.getClass();
          }
          if (introspector.getElementName(key) == null) {
            // The value of clazz determines if the qualification is written or not; Object.class generates the
            // qualification.
            key = new JAXBElement<Object>(new QName("key"), clazz, key);
          }
          StringWriter writer = new StringWriter();
          m.marshal(key, writer);
          jaxbEntry.key = format("key", writer.toString());
        }
        Object value = entry.getValue();
        if (value != null) {
          Class<Object> clazz = Object.class;
          if (valueAdapter != null) {
            value = valueAdapter.marshal(value);
            clazz = (Class<Object>) value.getClass();
          }
          if (introspector.getElementName(value) == null) {
            // The value of clazz determines if the qualification is written or not; Object.class generates the
            // qualification.
            value = new JAXBElement<Object>(new QName("value"), clazz, value);
          }
          StringWriter writer = new StringWriter();
          m.marshal(value, writer);
          jaxbEntry.value = format("value", writer.toString());
        }
      }
      return outWrapper;
    } else {
      throw new IllegalStateException("Adapter info could not be found.");
    }

  }

  private String format(String inTagName, String inXML) {
    String element = "<" + inTagName;
    // Remove unneeded namespaces, they are already declared in the top node.
    int beginIndex = inXML.indexOf(XSI_TYPE);
    if (beginIndex != -1) {
      int endIndex = inXML.indexOf(" ", beginIndex);
      element += " " + inXML.substring(beginIndex, endIndex) + " " + XSI_NS;
    }
    beginIndex = inXML.indexOf('>');
    element += inXML.substring(beginIndex);
    return CDATA_START + element + CDATA_END;
  }

  @XmlType(name = "map")
  static class Wrapper {

    @XmlElement(name = "mapClass")
    private String mapClass;

    @XmlElement(name = "field")
    private String field;

    @XmlElementWrapper(name = "map")
    @XmlElement(name = "entry")
    private final List<MapEntry> mapList = new ArrayList<MapEntry>();
  }

  @XmlType(name = "mapEntry")
  static class MapEntry {

    @XmlElement(name = "key")
    private String key;

    @XmlElement(name = "value")
    private String value;
  }

  public Marshaller.Listener getMarshallerListener() {
    return marshallerListener;
  }

  public Unmarshaller.Listener getUnmarshallerListener() {
    return unmarshallerListener;
  }

  private static class MarshallerListener extends Marshaller.Listener {

    private final List<Info> infoList = new ArrayList<Info>();

    @Override
    public void beforeMarshal(Object inSource) {
      extractInfo(infoList, inSource);
    }
  }

  private class UnmarshallerListener extends Unmarshaller.Listener {
    private final List<Info> infoList = new ArrayList<Info>();

    @Override
    public void beforeUnmarshal(Object inTarget, Object inParent) {
      extractInfo(infoList, inTarget);
    }
  }

  private static void extractInfo(List<Info> inList, Object inObject) {
    for (Field field : inObject.getClass().getDeclaredFields()) {
      for (Annotation a : field.getAnnotations()) {
        if (a.annotationType() == XmlJavaTypeAdapter.class) {
          if (((XmlJavaTypeAdapter) a).value() == MapAdapter.class) {
            MapKeyValueAdapters adapters = field.getAnnotation(MapKeyValueAdapters.class);
            if (adapters == null) {
              throw new IncompleteAnnotationException(XmlJavaTypeAdapter.class, "; XmlJavaTypeAdapter specifies "
                  + MapAdapter.class.getName() + " for field " + field.getName() + " in "
                  + inObject.getClass().getName() + ". This must be used in combination with annotation "
                  + MapKeyValueAdapters.class.getName());
            }
            try {
              field.setAccessible(true);
              Map<?, ?> value = (Map<?, ?>) field.get(inObject);
              if (value != null) {
                Info info = new Info();
                info.field = field.getName();
                info.map = value;
                info.adapters = adapters;
                inList.add(info);
              }
            } catch (Exception e) {
              throw new RuntimeException("Failed extracting annotation information from " + field.getName() + " in "
                  + inObject.getClass().getName(), e);
            }
          }
        }
      }
    }
  }

  private static class Info {
    private String field;
    private Map<?, ?> map;
    private MapKeyValueAdapters adapters;
  }
}
这可能会产生类似的输出

<testMap2>
    <mapClass>java.util.HashMap</mapClass>
    <field>testMap2</field>
    <map>
        <entry>
            <key><![CDATA[<key><number>1357</number><type>Unspecified</type></key>]]></key>
            <value><![CDATA[<value xsi:type="xs:string" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">gn</value>]]></value>
        </entry>
    </map>
</testMap2>

java.util.HashMap
testMap2
1357未指定]]>
gn]]>
可以看出,密钥不需要资格信息,因为我们已经知道要使用的适配器。 还要注意,我将CDATA添加到输出中。我已经实现了一个简单的字符转义处理程序,它尊重这一点(不包括在这个代码示例中)

由于我们的发布周期,在我们的代码中实现这一功能的机会出现之前,我还有一段时间,因此我认为最好与社区一起检查这个解决方案是否存在任何问题,或者在我忽略的JAXB规范中是否有更好的方法。我还假设代码中有一些部分可以以更好的方式完成。
感谢您的评论。

以下是我的解决方案:

  • 使地图成为暂时的
  • 使用包装列表进行编组
  • 需要时从列表中重新生成地图
  • 如果需要保持列表和地图同步,请使用添加(订单)功能
  • 具有订单地图的示例客户

      @XmlRootElement
      @XmlAccessorType(XmlAccessType.FIELD)
      public static class Order {
        @XmlID
        String orderId;
        String item;
        int count;
      }
    
      @XmlRootElement(name = "customer")
      @XmlAccessorType(XmlAccessType.FIELD)
      public static class Customer {
        String name;
        String firstname;
        @XmlElementWrapper(name = "orders")
        @XmlElement(name = "order")
        List<Order> orders = new ArrayList<Order>();
    
        @XmlTransient
        private Map<String, Order> ordermap = new LinkedHashMap<String, Order>();
    
        /**
         * reinitialize the order list
         */
        public void reinit() {
          for (Order order : orders) {
            ordermap.put(order.orderId, order);
          }
        }
    
        /**
         *  add the given order to the internal list and map
         *  @param order - the order to add
         */
        public void addOrder(Order order) {
          orders.add(order);
          ordermap.put(order.orderId,order);
        }
      }
    
    @XmlRootElement
    @XmlAccessorType(XmlAccessType.FIELD)
    公共静态类秩序{
    @XmlID
    字符串orderId;
    字符串项;
    整数计数;
    }
    @XmlRootElement(name=“客户”)
    @XmlAccessorType(XmlAccessType.FIELD)
    公共静态类客户{
    字符串名;
    字符串名;
    @xmlementwrapper(name=“orders”)
    @xmlement(name=“order”)
    列表顺序=新的ArrayList();
    @XmlTransient
    private Map ordermap=新建LinkedHashMap();
    /**
    *重新初始化订单列表
    */
    公共信息技术{
    对于(订单:订单){
    ordermap.put(order.orderId,order);
    }
    }
    /**
    *将给定的顺序添加到内部列表和映射中
    *@param order-要添加的顺序
    */
    公共无效添加命令(命令){
    订单。添加(订单);
    ordermap.put(order.orderId,order);
    }
    }
    
    示例XML

    <customer>
       <name>Doe</name>
       <firstname>John</firstname>
       <orders>
          <order>
             <orderId>Id1</orderId>
             <item>Item 1</item>
             <count>1</count>
          </order>
          <order>
             <orderId>Id2</orderId>
             <item>Item 2</item>
             <count>2</count>
          </order>
       </orders>
    </customer>
    
    
    雌鹿
    约翰
    Id1
    项目1
    1.
    Id2
    项目2
    2.
    
    最小完整和可验证示例

    例如

    可在

    这看起来很复杂,并且以非常专业的方式实现。这是不可原谅的,因为需要一个通用的jaxb映射包装器来工作。对映射的支持应该在JAXB中提供,而不需要一些附加代码。我希望这个问题最终会在将来的JAXB版本中得到解决关于我提出的解决方案,它使用的是公共API,因此在JAXB中找到合适的解决方案之前,它应该是未来的证明。请注意,我尝试了自动化同步并基于XMLID注释重新进行同步,但结果再次复杂化,因此目前我不再费心了。您好,非常感谢您的回复。我正在尝试
    unmarshal
    XML,但我不知道如何实现
    Map
    unmarshal
    方法。如果可能,请提供
    MapAdapter
    类中提到的
    unmarshal
    方法的示例:有关Java中XML支持的问题,请参阅。我已经转到python。
      @XmlRootElement
      @XmlAccessorType(XmlAccessType.FIELD)
      public static class Order {
        @XmlID
        String orderId;
        String item;
        int count;
      }
    
      @XmlRootElement(name = "customer")
      @XmlAccessorType(XmlAccessType.FIELD)
      public static class Customer {
        String name;
        String firstname;
        @XmlElementWrapper(name = "orders")
        @XmlElement(name = "order")
        List<Order> orders = new ArrayList<Order>();
    
        @XmlTransient
        private Map<String, Order> ordermap = new LinkedHashMap<String, Order>();
    
        /**
         * reinitialize the order list
         */
        public void reinit() {
          for (Order order : orders) {
            ordermap.put(order.orderId, order);
          }
        }
    
        /**
         *  add the given order to the internal list and map
         *  @param order - the order to add
         */
        public void addOrder(Order order) {
          orders.add(order);
          ordermap.put(order.orderId,order);
        }
      }
    
    <customer>
       <name>Doe</name>
       <firstname>John</firstname>
       <orders>
          <order>
             <orderId>Id1</orderId>
             <item>Item 1</item>
             <count>1</count>
          </order>
          <order>
             <orderId>Id2</orderId>
             <item>Item 2</item>
             <count>2</count>
          </order>
       </orders>
    </customer>