JAXB中的动态标签名

21

我正在使用Jersey和JAXB构建一个简单的RESTful Web服务。我有一个从“String”到“Integer”的HashMap:

2010-04 -> 24 
2010-05 -> 45

我需要生成一个类似于以下结构的XML响应:

 <map>
   <2010-04>24</2010-04>
   <2010-05>45</2010-05>
 </map>

使用JAXB生成动态标签名称的最佳方法是什么?


2
你确定要这样做吗?这几乎肯定是个坏主意。像<item month="2010-04">24</item>这样的东西更好、更容易处理。 - skaffman
同意,但不幸的是我们有一些现有的代码将使用该特定模式的文件,我正在尝试避免在那里引入任何新的更改...但如果JAXB无法实现这一点,添加名称属性将是次佳选择 :) - shane
你可以使用各种扩展/插件等方法来弯曲JAXB以适应你的需求,但这并不是这里的最佳选择。 - skaffman
4
XML中的标签名不能以数字开头。 - redben
3个回答

31

您可以使用带有@XmlAnyElement注释的属性,并将元素作为JAXBElement返回:

private Map<String, Integer> months = ...;

@XmlAnyElement
public List<JAXBElement<Integer>> getMonths() {
    List<JAXBElement<Integer>> elements = new ArrayList<JAXBElement<Integer>>();
    for (Map.Entry<String, Integer> month: months.entrySet()) 
        elements.add(new JAXBElement(new QName(month.getKey()), 
                                     Integer.class, month.getValue()));
    return elements;
}

这种方法很丑陋,但并不比它生成的XML更丑陋。


4
你如何反序列化这个? - les2

22

最近也遇到了这种问题。参考了上面axtavt的答案和其他一些问题线程后,我总结出了解决这种问题的步骤:

  1. 创建一个容器类来持有JAXBElement对象的列表(或数组),并使用@XmlAnyElement注释来表示可以动态生成元素名称。
  2. 创建一个XmlAdapter类,用于在Map和该容器类之间进行编组/解组操作。
  3. 对于Java bean中的任何Map字段,请使用@XmlJavaTypeAdapter进行注释,并将该XmlAdapter类作为其值进行设置(或者您可以直接使用容器类,如下所示)。

现在我以 Map<String, String> 为例说明:

{"key1": "value1", "key2": "value2"} 

将被编排成

<root>
    <key1>value1</key1>
    <key2>value2</key2>
</root>

以下是完整的代码段和注释,以及示例:

1、容器(用于 @XmlAnyElement)

/**
 * <dl>
 * <dt>References:
 * </dt>
 * <dd>
 *  <ul>
 *      <li><a href="http://stackoverflow.com/questions/21382202/use-jaxb-xmlanyelement-type-of-style-to-return-dynamic-element-names">Dynamic element names in JAXB</a></li>
 *      <li><a href="https://dev59.com/MW865IYBdhLWcg3wLrlE">Marshal Map into key-value pairs</a></li>
 *      <li><a href="https://dev59.com/zHA75IYBdhLWcg3wdIl0">Dynamic tag names with JAXB</a></li>
 *  </ul>
 * </dd>
 * </dl>
 * @author MEC
 *
 */
@XmlType
public static class MapWrapper{
    private List<JAXBElement<String>> properties = new ArrayList<>();

    public MapWrapper(){

    }
    /**
     * <p>
     * Funny fact: due to type erasure, this method may return 
     * List<Element> instead of List<JAXBElement<String>> in the end;
     * </p>
     * <h4>WARNING: do not use this method in your programme</h4>
     * <p>
     * Thus to retrieve map entries you've stored in this MapWrapper, it's 
     * recommended to use {@link #toMap()} instead.
     * </p>
     * @return
     */
    @XmlAnyElement
    public List<JAXBElement<String>> getProperties() {
        return properties;
    }
    public void setProperties(List<JAXBElement<String>> properties) {
        this.properties = properties;
    }




    /**
     * <p>
     * Only use {@link #addEntry(JAXBElement)} and {{@link #addEntry(String, String)}
     * when this <code>MapWrapper</code> instance is created by yourself 
     * (instead of through unmarshalling).
     * </p>
     * @param key map key
     * @param value map value
     */
    public void addEntry(String key, String value){
        JAXBElement<String> prop = new JAXBElement<String>(new QName(key), String.class, value);
        addEntry(prop);
    }
    public void addEntry(JAXBElement<String> prop){
        properties.add(prop);
    }

    @Override
    public String toString() {
        return "MapWrapper [properties=" + toMap() + "]";
    }

    /**
     * <p>
     * To Read-Only Map
     * </p>
     * 
     * @return
     */
    public Map<String, String> 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));
    }


    /**
     * <p>
     * Extract local name from <code>obj</code>, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element;
     * </p>
     * @param obj
     * @return
     */
    @SuppressWarnings("unchecked")
    private static String extractLocalName(Object obj){

        Map<Class<?>, Function<? super Object, String>> strFuncs = new HashMap<>();
        strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement<String>)jaxb).getName().getLocalPart());
        strFuncs.put(Element.class, ele -> ((Element) ele).getLocalName());
        return extractPart(obj, strFuncs).orElse("");
    }

    /**
     * <p>
     * Extract text content from <code>obj</code>, whether it's javax.xml.bind.JAXBElement or org.w3c.dom.Element;
     * </p>
     * @param obj
     * @return
     */
    @SuppressWarnings("unchecked")
    private static String extractTextContent(Object obj){
        Map<Class<?>, Function<? super Object, String>> strFuncs = new HashMap<>();
        strFuncs.put(JAXBElement.class, (jaxb) -> ((JAXBElement<String>)jaxb).getValue());
        strFuncs.put(Element.class, ele -> ((Element) ele).getTextContent());
        return extractPart(obj, strFuncs).orElse("");
    }

    /**
     * Check class type of <code>obj</code> according to types listed in <code>strFuncs</code> keys,
     * then extract some string part from it according to the extract function specified in <code>strFuncs</code>
     * values.
     * @param obj
     * @param strFuncs
     * @return
     */
    private static <ObjType, T> Optional<T> extractPart(ObjType obj, Map<Class<?>, 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();
    }
}

注:

  1. 对于JAXB绑定,你需要关注的仅是这个getProperties方法,它被@XmlAnyElement注解标记。
  2. 为了方便使用,在这里引入了两个addEntry方法。但是要小心使用,因为当它们用于通过JAXBContext(而不是通过new操作符创建)新反编组的MapWrapper时,情况可能变得非常糟糕。
  3. toMap在这里被引入作为信息探针,即帮助检查存储在此MapWrapper实例中的映射条目。

2、适配器(XmlAdapter)

XmlAdapter@XmlJavaTypeAdapter成对使用,只有当Map<String,String>被用作bean属性时才需要使用。

/**
 * <p>
 * ref: http://stackoverflow.com/questions/21382202/use-jaxb-xmlanyelement-type-of-style-to-return-dynamic-element-names
 * </p>
 * @author MEC
 *
 */
public static class MapAdapter extends XmlAdapter<MapWrapper, Map<String, String>>{

    @Override
    public Map<String, String> unmarshal(MapWrapper v) throws Exception {
        Map<String, String> map = v.toMap();

        return map;
    }

    @Override
    public MapWrapper marshal(Map<String, String> m) throws Exception {
        MapWrapper wrapper = new MapWrapper();

        for(Map.Entry<String, String> entry : m.entrySet()){
             wrapper.addEntry(new JAXBElement<String>(new QName(entry.getKey()), String.class, entry.getValue()));
        }

        return wrapper;
    }

}

3,示例

以下是两个示例,展示了容器和适配器的使用。

3.1 示例1

将此XML映射为:

<root>
    <key1>value1</key1>
    <key2>value2</key2>
<root>

您可以使用以下类:

@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());
请提供需要翻译的完整内容,以便我能准确回答您的请求。
<root>
    <map>
        <key1>value1</key1>
        <key2>value2</key2>
    </map>
    <other>other content</other>
</root>

您可以使用以下类:

@XmlRootElement(name="root")
@XmlType(propOrder={"map", "other"})
public class YetAnotherBean{
    private Map<String, String> 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<String, String> getMap(){
        return map;
    }
    public void setMap(Map<String, String> 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 应用于具有 MapAdapter 作为其值的 Map<String, String> 字段。

3.3 示例3

现在让我们向这些元素添加一些属性。由于某些神秘的原因,我有这种XML结构需要进行映射:

<sys-config>
  <sys-params>
    <ACCESSLOG_FILE_BY attr="C" desc="AccessLog file desc">SYSTEM</ACCESSLOG_FILE_BY>
    <ACCESSLOG_WRITE_MODE attr="D" desc="">DB</ACCESSLOG_WRITE_MODE>
    <CHANEG_BUTTON_IMAGES attr="E" desc="Button Image URL, eh, boolean value. ...Wait, what?">FALSE</CHANEG_BUTTON_IMAGES>
  </sys-params>
</sys-config>

如您所见,系统参数名称都设置为元素的名称而不是其属性。为了解决这个问题,我们可以再次使用稍微的帮助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{
    /**
     * <p>
     * Here is the tricky part:
     * <ul>
     *  <li>When this <code>SysParamEntriesWrapper</code> is created by yourself, objects 
     * stored in this <code>entries</code> list is of type SystemParamEntry</li>
     *  <li>Yet during the unmarshalling process, this <code>SysParamEntriesWrapper</code> is 
     * created by the JAXBContext, thus objects stored in the <code>entries</code> is 
     * of type Element actually.</li>
     * </ul>
     * </p>
     */
    List<JAXBElement<SysParamEntry>> 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<SysParamEntry> bean = new JAXBElement<SysParamEntry>(new QName("", entry.getName()), SysParamEntry.class, entry);
        entries.add(bean);
    }

    @XmlAnyElement
    public List<JAXBElement<SysParamEntry>> getEntries() {
        return entries;
    }
    public void setEntries(List<JAXBElement<SysParamEntry>> entries) {
        this.entries = entries;
    }


    @Override
    public String toString() {
        return "SysParammEntriesWrapper [entries=" + toMap() + "]";
    }


    public Map<String, SysParamEntry> toMap(){
        Map<String, SysParamEntry> 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 <ObjType, RetType> Optional<RetType> extractPart(ObjType obj, Map<Class<?>,
            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<Class<?>, Function<? super Object, String>> nameExtractors = new HashMap<>();
    private static Map<Class<?>, Function<? super Object, String>> attrExtractors = new HashMap<>();
    private static Map<Class<?>, Function<? super Object, String>> descExtractors = new HashMap<>();
    private static Map<Class<?>, Function<? super Object, String>> valueExtractors = new HashMap<>();
    static{
        nameExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getName().getLocalPart());
        nameExtractors.put(Element.class, ele -> ((Element) ele).getLocalName());

        attrExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getAttr());
        attrExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("attr"));

        descExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getDesc());
        descExtractors.put(Element.class, ele -> ((Element) ele).getAttribute("desc"));

        valueExtractors.put(JAXBElement.class, jaxb -> ((JAXBElement<SysParamEntry>)jaxb).getValue().getValue());
        valueExtractors.put(Element.class, ele -> ((Element) ele).getTextContent());
    }
}

public class SysParamEntriesAdapter extends XmlAdapter<SysParamEntriesWrapper, SysParamEntries>{

    @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<SysParamEntry> entries = new ArrayList<>();;
    public SysParamEntries(){

    }
    public SysParamEntries(List<SysParamEntry> 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<SysParamEntry> getEntries() {
        return entries;
    }
    public void setEntries(List<SysParamEntry> 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());

在示例2中,我想要这种类型的XML生成:<root> value1 value2 其他内容</root>。只是不想要额外的<map></map>标签。我能实现这个吗? - pankaj
非常有用的信息。已经点赞了!还有一个问题:如果我的XML结构具有动态子字段,如下所示:<sys-config> <sys-params> <ACCESSLOG_FILE_BY attr="C" desc="AccessLog文件描述">SYSTEM</ACCESSLOG_FILE_BY> <ACCESSLOG_WRITE_MODE attr="D" desc="">DB</ACCESSLOG_WRITE_MODE> <CHANEG_BUTTON_IMAGES attr="E" desc="按钮图像URL,布尔值...等等,等等">FALSE</CHANEG_BUTTON_IMAGES> <author> <personnel_info attr="姓名 姓氏" desc="这是一个人">导出</personnel_info> </author> </sys-params> </sys-config> - saygley

5
也许有人对使用马歇尔和取消马歇尔示例的更简单解决方案感兴趣。 这不是一个映射,但仍然是一个键值解决方案,因为我们使用带有键(=localname)和值(=textcontent)的 JAXBElement
@XmlRootElement(name="map")
@XmlAccessorType(XmlAccessType.FIELD)
public class XmlMap {
    //one caveat (as mec_test_1 pointed out) unmarshalled objects are from type org.w3c.dom.Element and during marshall it is JAXBElement
    @XmlAnyElement
    List<JAXBElement<String>> dates = new ArrayList<>();

现在我们需要解析一个XML文件:

<map>
   <2019-01-01>Yes</2019-01-01>
   <2019-02-01>No</2019-02-01>
</map>

您需要运行以下命令:
JAXBContext c = JAXBContext.newInstance(XmlMap.class);
XmlMap map = c.createUnmarshaller().unmarshall(new File("xmlfile.xml"));
//access the objects via
System.out.println("Key: " + ((org.w3c.dom.Element) map.dates.get(0)).getLocalName());
System.out.println("Value: " + ((org.w3c.dom.Element) map.dates.get(0)).getTextContent());

为了整理一个对象,可以使用“安排(An)”函数:
import javax.xml.namespace.QName;
import javax.xml.bind.JAXBElement;

XmlMap xmlMap = new XmlMap();
xmlMap.dates.add(new JAXBElement<String>(new QName("key"), String.class, "value"));
xmlMap.dates.add(new JAXBElement<String>(new QName("2019-01-01"), String.class, "Yes"));

JAXBContext context = JAXBContext.newInstance(XmlMap.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
m.marshal(verzObj, System.out);

输出:

<map>
   <key>val</key>
   <2019-01-01>Yes</2019-01-01>
</map>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接