使用JAXB的动态标记名称
我正在使用Jersey和JAXB来构建一个简单的RESTful Web服务,我有一个’String’的HashMap到’Integer’:
2010-04 -> 24 2010-05 -> 45
我需要生成一个XML响应,如下所示:
使用JAXB生成动态标记名称的最佳方法是什么?
您可以使用@XmlAnyElement
-annotated属性并将元素作为JAXBElement
返回:
private Map months = ...; @XmlAnyElement public List> getMonths() { List> elements = new ArrayList>(); for (Map.Entry month: months.entrySet()) elements.add(new JAXBElement(new QName(month.getKey()), Integer.class, month.getValue())); return elements; }
这种方法很丑陋,但并不比它产生的XML更丑陋。
最近也遇到了这种问题。 在引用上面列出的axtavt的答案(以及一堆其他问题线程)后,我对这类问题做了总结:
- 一个容器类,它包含
JAXBElement
对象的列表(或数组),其中此列表(或数组)使用@XmlAnyElement
注释,因此可以生成动态元素名称。 - 一个
XmlAdapter
类,用于处理Map与/或此容器类之间的编组/解组。 - 使用
@XmlJavaTypeAdapter
注释java bean的任何Map字段,并将此XmlAdapter
类作为其值(或者您可以直接使用容器类,如下所示)。
现在我将把Map
作为例子,在这里
{"key1": "value1", "key2": "value2"}
将被整理进去
value1 value2
以下是完整的代码段和注释,以及示例:
1,容器(用于@XmlAnyElement)
/** * * - References: *
* - * *
*
* @author MEC * */ @XmlType public static class MapWrapper{ private List> properties = new ArrayList<>(); public MapWrapper(){ } /** * * Funny fact: due to type erasure, this method may return * List instead of List> in the end; *
* WARNING: do not use this method in your programme
* * Thus to retrieve map entries you've stored in this MapWrapper, it's * recommended to use {@link #toMap()} instead. *
* @return */ @XmlAnyElement public List> getProperties() { return properties; } public void setProperties(List> properties) { this.properties = properties; } /** * * Only use {@link #addEntry(JAXBElement)} and {{@link #addEntry(String, String)} * when this MapWrapper
instance is created by yourself * (instead of through unmarshalling). *
* @param key map key * @param value map value */ public void addEntry(String key, String value){ JAXBElement prop = new JAXBElement (new QName(key), String.class, value); addEntry(prop); } public void addEntry(JAXBElement prop){ properties.add(prop); } @Override public String toString() { return "MapWrapper [properties=" + toMap() + "]"; } /** * * To Read-Only Map *
* * @return */ public Map toMap(){ //Note: Due to type erasure, you cannot use properties.stream() directly when unmashalling is used.. List> props = properties; return props.stream().collect(Collectors.toMap(MapWrapper::extractLocalName, MapWrapper::extractTextContent)); } /** * * Extract local name from obj
, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element; *
* @param obj * @return */ @SuppressWarnings("unchecked") private static String extractLocalName(Object obj){ Map, Function super Object, String>> strFuncs = new HashMap<>(); strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement )jaxb).getName().getLocalPart()); strFuncs.put(Element.class, ele -> ((Element) ele).getLocalName()); return extractPart(obj, strFuncs).orElse(""); } /** * * Extract text content from obj
, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element; *
* @param obj * @return */ @SuppressWarnings("unchecked") private static String extractTextContent(Object obj){ Map, Function super Object, String>> strFuncs = new HashMap<>(); strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement )jaxb).getValue()); strFuncs.put(Element.class, ele -> ((Element) ele).getTextContent()); return extractPart(obj, strFuncs).orElse(""); } /** * Check class type of obj
according to types listed in strFuncs
keys, * then extract some string part from it according to the extract function specified in strFuncs
* values. * @param obj * @param strFuncs * @return */ private static Optional extractPart(ObjType obj, Map, Function super ObjType, T>> strFuncs){ for(Class> clazz : strFuncs.keySet()){ if(clazz.isInstance(obj)){ return Optional.of(strFuncs.get(clazz).apply(obj)); } } return Optional.empty(); } }
笔记:
- 对于JAXB绑定,您需要注意的是此
getProperties
方法,该方法由@XmlAnyElement
注释。 - 这里引入了两个
addEntry
方法,以方便使用。 应该谨慎使用它们,因为当它们通过JAXBContext
用于新编组的MapWrapper
(而不是由你自己通过一个new
运算符创建),可能会发现可怕的错误。 - 这里介绍
toMap
用于信息探测,即帮助检查存储在此MapWrapper
实例中的映射条目。
2,适配器(XmlAdapter)
XmlAdapter
与@XmlJavaTypeAdapter
成对使用,在这种情况下,仅当Map
用作bean属性时才需要它。
/** * * ref: http://stackoverflow.com/questions/21382202/use-jaxb-xmlanyelement-type-of-style-to-return-dynamic-element-names *
* @author MEC * */ public static class MapAdapter extends XmlAdapter>{ @Override public Map unmarshal(MapWrapper v) throws Exception { Map map = v.toMap(); return map; } @Override public MapWrapper marshal(Map m) throws Exception { MapWrapper wrapper = new MapWrapper(); for(Map.Entry entry : m.entrySet()){ wrapper.addEntry(new JAXBElement(new QName(entry.getKey()), String.class, entry.getValue())); } return wrapper; } }
3,例子
以下是显示容器和适配器用法的两个示例。
3.1例1
要映射这个xml:
value1 value2
您可以使用以下类:
@XmlRootElement(name="root") public class CustomMap extends MapWrapper{ public CustomMap(){ } }
测试代码:
CustomMap map = new CustomMap(); map.addEntry("key1", "value1"); map.addEntry("key1", "value2"); StringWriter sb = new StringWriter(); JAXBContext.newInstance(CustomMap.class).createMarshaller().marshal(map, sb); out.println(sb.toString());
请注意,此处不使用@XmlJavaTypeAdapter
。
3.2例2
要映射这个xml:
other content
您可以使用以下类:
@XmlRootElement(name="root") @XmlType(propOrder={"map", "other"}) public class YetAnotherBean{ private Map map = new HashMap<>(); private String other; public YetAnotherBean(){ } public void putEntry(String key, String value){ map.put(key, value); } @XmlElement(name="map") @XmlJavaTypeAdapter(MapAdapter.class) public Map getMap(){ return map; } public void setMap(Map map){ this.map = map; } @XmlElement(name="other") public String getOther(){ return other; } public void setOther(String other){ this.other = other; } }
测试代码:
YetAnotherBean yab = new YetAnotherBean(); yab.putEntry("key1", "value1"); yab.putEntry("key2", "value2"); yab.setOther("other content"); StringWriter sb = new StringWriter(); JAXBContext.newInstance(YetAnotherBean.class).createMarshaller().marshal(yab, sb); out.println(sb.toString());
请注意, @XmlJavaTypeAdapter
应用于Map
字段,其中MapAdapter
作为其值。
3.3例3
现在让我们为这些元素添加一些属性。 由于一些神秘的原因,我有这种XML结构来映射:
SYSTEM DB FALSE
如您所见,系统参数名称都设置为元素的名称而不是其属性。 要解决此问题,我们可以再次使用JAXBElement
的一些帮助:
@XmlRootElement(name="sys-config") public class SysParamConfigXDO{ private SysParamEntries sysParams = new SysParamEntries(); public SysParamConfigXDO(){ } public void addSysParam(String name, String value, String attr, String desc){ sysParams.addEntry(name, value, attr, desc);; } @XmlElement(name="sys-params") @XmlJavaTypeAdapter(SysParamEntriesAdapter.class) public SysParamEntries getSysParams() { return sysParams; } public void setSysParams(SysParamEntries sysParams) { this.sysParams = sysParams; } @Override public String toString() { return "SysParamConfigXDO [sysParams=" + sysParams + "]"; } } @XmlRootElement(name="root") public class SysParamXDO extends SysParamEntriesWrapper{ public SysParamXDO(){ } } @SuppressWarnings("unchecked") @XmlType public class SysParamEntriesWrapper{ /** * * Here is the tricky part: *
* - When this
SysParamEntriesWrapper
is created by yourself, objects * stored in this entries
list is of type SystemParamEntry * - Yet during the unmarshalling process, this
SysParamEntriesWrapper
is * created by the JAXBContext, thus objects stored in the entries
is * of type Element actually. *
* */ List> entries = new ArrayList<>(); public SysParamEntriesWrapper(){ } public void addEntry(String name, String value, String attr, String desc){ addEntry(new SysParamEntry(name, value, attr, desc)); } public void addEntry(String name, String value){ addEntry(new SysParamEntry(name, value)); } public void addEntry(SysParamEntry entry){ JAXBElement bean = new JAXBElement (new QName("", entry.getName()), SysParamEntry.class, entry); entries.add(bean); } @XmlAnyElement public List> getEntries() { return entries; } public void setEntries(List> entries) { this.entries = entries; } @Override public String toString() { return "SysParammEntriesWrapper [entries=" + toMap() + "]"; } public Map toMap(){ Map retval = new HashMap<>(); List> entries = this.entries; entries.stream().map(SysParamEntriesWrapper::convertToParamEntry). forEach(entry -> retval.put(entry.getName(), entry));; return retval; } private static SysParamEntry convertToParamEntry(Object entry){ String name = extractName(entry); String attr = extractAttr(entry); String desc = extractDesc(entry); String value = extractValue(entry); return new SysParamEntry(name, value, attr, desc); } @SuppressWarnings("unchecked") private static String extractName(Object entry){ return extractPart(entry, nameExtractors).orElse(""); } @SuppressWarnings("unchecked") private static String extractAttr(Object entry){ return extractPart(entry, attrExtractors).orElse(""); } @SuppressWarnings("unchecked") private static String extractDesc(Object entry){ return extractPart(entry, descExtractors).orElse(""); } @SuppressWarnings("unchecked") private static String extractValue(Object entry){ return extractPart(entry, valueExtractors).orElse(""); } private static Optional extractPart(ObjType obj, Map, Function super ObjType, RetType>> extractFuncs ){ for(Class> clazz : extractFuncs.keySet()){ if(clazz.isInstance(obj)){ return Optional.ofNullable(extractFuncs.get(clazz).apply(obj)); } } return Optional.empty(); } private static Map, Function super Object, String>> nameExtractors = new HashMap<>(); private static Map, Function super Object, String>> attrExtractors = new HashMap<>(); private static Map, Function super Object, String>> descExtractors = new HashMap<>(); private static Map, Function super Object, String>> valueExtractors = new HashMap<>(); static{ nameExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement)jaxb).getName().getLocalPart()); nameExtractors.put(Element.class, ele -> ((Element) ele).getLocalName()); attrExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement )jaxb).getValue().getAttr()); attrExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("attr")); descExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement )jaxb).getValue().getDesc()); descExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("desc")); valueExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement )jaxb).getValue().getValue()); valueExtractors.put(Element.class, ele -> ((Element) ele).getTextContent()); } } public class SysParamEntriesAdapter extends XmlAdapter{ @Override public SysParamEntries unmarshal(SysParamEntriesWrapper v) throws Exception { SysParamEntries retval = new SysParamEntries(); v.toMap().values().stream().forEach(retval::addEntry); return retval; } @Override public SysParamEntriesWrapper marshal(SysParamEntries v) throws Exception { SysParamEntriesWrapper entriesWrapper = new SysParamEntriesWrapper(); v.getEntries().forEach(entriesWrapper::addEntry); return entriesWrapper; } } public class SysParamEntries{ List entries = new ArrayList<>();; public SysParamEntries(){ } public SysParamEntries(List entries) { super(); this.entries = entries; } public void addEntry(SysParamEntry entry){ entries.add(entry); } public void addEntry(String name, String value){ addEntry(name, value, "C"); } public void addEntry(String name, String value, String attr){ addEntry(name, value, attr, ""); } public void addEntry(String name, String value, String attr, String desc){ entries.add(new SysParamEntry(name, value, attr, desc)); } public List getEntries() { return entries; } public void setEntries(List entries) { this.entries = entries; } @Override public String toString() { return "SystemParamEntries [entries=" + entries + "]"; } } @XmlType public class SysParamEntry{ String name; String value = ""; String attr = ""; String desc = ""; public SysParamEntry(){ } public SysParamEntry(String name, String value) { super(); this.name = name; this.value = value; } public SysParamEntry(String name, String value, String attr) { super(); this.name = name; this.value = value; this.attr = attr; } public SysParamEntry(String name, String value, String attr, String desc) { super(); this.name = name; this.value = value; this.attr = attr; this.desc = desc; } @XmlTransient public String getName() { return name; } public void setName(String name) { this.name = name; } @XmlValue public String getValue() { return value; } public void setValue(String value) { this.value = value; } @XmlAttribute(name="attr") public String getAttr() { return attr; } public void setAttr(String attr) { this.attr = attr; } @XmlAttribute(name="desc") public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } @Override public String toString() { return "SystemParamEntry [name=" + name + ", value=" + value + ", attr=" + attr + ", desc=" + desc + "]"; } }
现在是测试的时候了:
//Marshal SysParamConfigXDO xdo = new SysParamConfigXDO(); xdo.addSysParam("ACCESSLOG_FILE_BY", "SYSTEM", "C", "AccessLog file desc"); xdo.addSysParam("ACCESSLOG_WRITE_MODE", "DB", "D", ""); xdo.addSysParam("CHANEG_BUTTON_IMAGES", "FALSE", "E", "Button Image URL, eh, boolean value. ...Wait, what?"); JAXBContext jaxbCtx = JAXBContext.newInstance(SysParamConfigXDO.class, SysParamEntries.class); jaxbCtx.createMarshaller().marshal(xdo, System.out); //Unmarshal Path xmlFile = Paths.get("path_to_the_saved_xml_file.xml"); JAXBContext jaxbCtx = JAXBContext.newInstance(SysParamConfigXDO.class, SysParamEntries.class); SysParamConfigXDO xdo = (SysParamConfigXDO) jaxbCtx.createUnmarshaller().unmarshal(xmlFile.toFile()); out.println(xdo.toString());