JAXB的通用映射

And*_*son 5 java jaxb

好的,所以我们都知道地图在JAXB中有些痛苦.我在这里提出了当前解决方案的替代方案.我的主要目标是获得有关此解决方案的任何和所有潜在问题的反馈.由于某些原因,它可能甚至不是一个好的解决方案.

当我使用标准的通用映射适配器时,似乎没有使用类的适配器.这些类被扫描,迫使我用JAXB注释标记我的数据模型,并添加我不想要它们的默认构造函数(我说的是我想要存储在Maps中的复杂类,而不是简单的数据类型).最重要的是,这使得我的内部数据模型公开,从而破坏了封装,因为生成的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>();
Run Code Online (Sandbox Code Playgroud)

此附加注释同时接受key和value作为参数.如果省略,则功能将依赖于省略的标准限定.上面的示例将使用给定的适配器进行值的键和标准处理.这里是注释.

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.xml.bind.annotation.adapters.XmlAdapter;

/**
 * This annotation holds the adapters for the key and value used in the MapAdapter.
 */
@Retention(RUNTIME)
@Target({ FIELD })
public @interface MapKeyValueAdapters {

  /**
   * Points to the class that converts the value type to a bound type or vice versa. See {@link XmlAdapter} for more
   * details.
   */
  Class<? extends XmlAdapter<?, ?>> key() default UNDEFINED.class;

  /**
   * Points to the class that converts the value type to a bound type or vice versa. See {@link XmlAdapter} for more
   * details.
   */
  Class<? extends XmlAdapter<?, ?>> value() default UNDEFINED.class;

  static final class UNDEFINED extends XmlAdapter<String, String> {

    @Override
    public String unmarshal(String v) throws Exception {
      return null;
    }

    @Override
    public String marshal(String v) throws Exception {
      return null;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

在这里适配器

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,只要适配器具有默认构造函数,该适配器就能够处理所有类型的映射.

最后是设置适配器用法的代码.

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

/**
 * Singleton that manages the JAXB functionality.
 */
public enum JAXBManager {

  INSTANCE;

  private JAXBContext context;

  private JAXBManager() {
    try {
      context = JAXBContext.newInstance(SomeComplexClass.class.getPackage().getName());
    } catch (JAXBException e) {
      throw new RuntimeException(e);
    }
  }
  public Marshaller createMarshaller() throws JAXBException {
    Marshaller m = context.createMarshaller();
    MapAdapter adapter = new MapAdapter(context);
    m.setAdapter(adapter);
    m.setListener(adapter.getMarshallerListener());
    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
    return m;
  }

  public Unmarshaller createUnmarshaller() throws JAXBException {
    Unmarshaller um = context.createUnmarshaller();
    MapAdapter adapter = new MapAdapter(context);
    um.setAdapter(adapter);
    um.setListener(adapter.getUnmarshallerListener());
    return um;
  }
}
Run Code Online (Sandbox Code Playgroud)

这可能会产生类似的输出

<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>
Run Code Online (Sandbox Code Playgroud)

可以看出,密钥不需要资格信息,因为我们已经知道要使用的适配器.另请注意,我将CDATA添加到输出中.我已经实现了一个尊重这个的简单字符转义处理程序(不包含在此代码示例中).

由于我们的发布周期,我有机会在我们的代码中实现此功能之前有一些时间,因此我认为,如果此解决方案存在任何问题或者已经有更好的方法,请与社区核实是明智的在我忽略的JAXB规范中.我还假设代码的某些部分可以用更好的方式完成.感谢您的评论.

Wol*_*ahl 2

这是我对解决方法的建议:

  1. 使地图 XmlTransient
  2. 使用包装列表进行编组
  3. 需要时从列表中重新初始化地图
  4. 如果您需要保持列表和地图同步,请使用 add(order) 函数

具有订单地图的示例客户

  @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);
    }
  }
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

最小完整且可验证的示例

一个例子根据

可以在https://github.com/BITPlan/com.bitplan.simplerest/blob/master/src/test/java/com/bitplan/jaxb/TestJaxbFactory.java#L390找到