You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java 8 JAXB泛型反序列化同名XML元素问题求助

JAXB泛型反序列化同名XML元素映射问题

问题背景

调用多个返回XML数据的API,接口返回的XML结构高度相似,仅存在一对同名元素:一个是<header>的子元素,另一个是<root>的子元素。例如:

  • User.xml中为<userAPI>
  • Order.xml中为<orderAPI>

示例XML

User.xml

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <header>
        <userAPI info="User info"/>
    </header>
    <userAPI>
        <username>charles</username>
    </userAPI>
</root>

Order.xml

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <header>
        <orderAPI info="Order info"/>
    </header>
    <orderAPI>
        <orderId>1</orderId>
    </orderAPI>
</root>

当前实现(Lombok + JAXB泛型)

pom.xml依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

UserApiHeader.java

@Data
@XmlRootElement(name = "userAPI")
@XmlAccessorType(XmlAccessType.FIELD)
public class UserApiHeader {
    @XmlAttribute
    private String info;
}

UserApi.java

@Data
@XmlRootElement(name = "userAPI")
@XmlAccessorType(XmlAccessType.FIELD)
public class UserApi {
    @XmlElement
    private String username;
}

Header.java

@Data
@XmlRootElement(name = "header")
@XmlAccessorType(XmlAccessType.FIELD)
public class Header<T> {
    @XmlAnyElement(lax = true)
    private T headerDetail;
}

Root.java

@Data
@XmlRootElement(name = "root")
@XmlAccessorType(XmlAccessType.FIELD)
public class Root<T, E> {
    @XmlElement
    private Header<T> header;
    @XmlAnyElement(lax = true)
    private E body;
}

Main.java反序列化代码

public class Main {
    public static void main(String[] args) {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"
                + "<root>\r\n"
                + "    <header>\r\n"
                + "        <userAPI info=\"User info\"/>\r\n"
                + "    </header>\r\n"
                + "    <userAPI>\r\n"
                + "        <username>charles</username>\r\n"
                + "    </userAPI>\r\n"
                + "</root>";
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(Root.class, UserApi.class, UserApiHeader.class);
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            @SuppressWarnings("unchecked")
            Root<UserApiHeader, UserApi> root = (Root<UserApiHeader, UserApi>) unmarshaller.unmarshal(new StringReader(xml));
            System.out.println("Header detail: " + root.getHeader().getHeaderDetail());
            System.out.println("Body:          " + root.getBody());
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }
}

问题现象

运行上述代码,结果如下:

Header detail: UserApiHeader(info=User info)
Body:          UserApiHeader(info=null)

两个<userAPI>元素均被映射到UserApiHeader类,导致body部分反序列化失败。

若调换JAXBContext初始化时的类顺序:

JAXBContext jaxbContext = JAXBContext.newInstance(Root.class, UserApiHeader.class, UserApi.class);

结果变为:

Header detail: UserApi(username=null)
Body:          UserApi(username=charles)

此时header部分的元素被错误映射到UserApi类,反序列化失败。

API返回的XML结构无法修改,只能调整本地代码逻辑。

补充:序列化正常

使用相同类进行序列化时,生成的XML与API返回结构完全一致:

public class Main {
    //...
    
    static void marshal() {
        UserApiHeader userApiHeader = new UserApiHeader();
        userApiHeader.setInfo("User info");
        Header<UserApiHeader> header = new Header<UserApiHeader>();
        header.setHeaderDetail(userApiHeader);
        
        UserApi userApi = new UserApi();
        userApi.setUsername("charles");
        Root<UserApiHeader, UserApi> root = new Root<UserApiHeader, UserApi>();
        root.setHeader(header);
        root.setBody(userApi);
        
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(Root.class, UserApi.class, UserApiHeader.class);
            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            StringWriter stringWriter = new StringWriter();
            marshaller.marshal(root, stringWriter);
            String xml = stringWriter.toString();
            System.out.println(xml);
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }
}

解决方案

问题根源在于JAXB对同名元素仅会根据JAXBContext中注册类的顺序选择第一个匹配的@XmlRootElement类映射,无法区分元素的父节点位置。以下两种方案可解决该问题:

方案一:利用@XmlElementRef和@XmlRegistry区分节点位置

  1. 移除UserApiHeader和UserApi上的@XmlRootElement,改用ObjectFactory注册带作用域的元素声明:
// UserApiHeader.java
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public class UserApiHeader {
    @XmlAttribute
    private String info;
}

// UserApi.java
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public class UserApi {
    @XmlElement
    private String username;
}

// ObjectFactory.java
@XmlRegistry
public class ObjectFactory {
    @XmlElementDecl(name = "userAPI", scope = Header.class)
    public JAXBElement<UserApiHeader> createHeaderUserAPI(UserApiHeader value) {
        return new JAXBElement<>(new QName("userAPI"), UserApiHeader.class, Header.class, value);
    }

    @XmlElementDecl(name = "userAPI", scope = Root.class)
    public JAXBElement<UserApi> createRootUserAPI(UserApi value) {
        return new JAXBElement<>(new QName("userAPI"), UserApi.class, Root.class, value);
    }
}
  1. 修改Header和Root类,使用@XmlElementRef替代@XmlAnyElement:
// Header.java
@Data
@XmlRootElement(name = "header")
@XmlAccessorType(XmlAccessType.FIELD)
public class Header<T> {
    @XmlElementRef
    private JAXBElement<T> headerDetail;
}

// Root.java
@Data
@XmlRootElement(name = "root")
@XmlAccessorType(XmlAccessType.FIELD)
public class Root<T, E> {
    @XmlElement
    private Header<T> header;
    @XmlElementRef
    private JAXBElement<E> body;
}
  1. 初始化JAXBContext时加入ObjectFactory:
JAXBContext jaxbContext = JAXBContext.newInstance(Root.class, ObjectFactory.class);

通过scope参数,JAXB可区分不同父节点下的同名元素,分别映射到对应类。

方案二:自定义XmlAdapter判断节点位置

  1. 创建通用适配器,根据元素父节点名称选择反序列化类:
public class ApiElementAdapter extends XmlAdapter<Object, Object> {
    @Override
    public Object unmarshal(Object v) throws Exception {
        if (v instanceof Element) {
            Element element = (Element) v;
            String parentNodeName = element.getParentNode().getNodeName();
            String elementName = element.getNodeName();
            
            if ("header".equals(parentNodeName)) {
                if ("userAPI".equals(elementName)) {
                    JAXBContext ctx = JAXBContext.newInstance(UserApiHeader.class);
                    return ctx.createUnmarshaller().unmarshal(element);
                } else if ("orderAPI".equals(elementName)) {
                    JAXBContext ctx = JAXBContext.newInstance(OrderApiHeader.class);
                    return ctx.createUnmarshaller().unmarshal(element);
                }
            } else if ("root".equals(parentNodeName)) {
                if ("userAPI".equals(elementName)) {
                    JAXBContext ctx = JAXBContext.newInstance(UserApi.class);
                    return ctx.createUnmarshaller().unmarshal(element);
                } else if ("orderAPI".equals(elementName)) {
                    JAXBContext ctx = JAXBContext.newInstance(OrderApi.class);
                    return ctx.createUnmarshaller().unmarshal(element);
                }
            }
        }
        return null;
    }

    @Override
    public Object marshal(Object v) throws Exception {
        JAXBContext ctx = JAXBContext.newInstance(v.getClass());
        Marshaller marshaller = ctx.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
        StringWriter writer = new StringWriter();
        marshaller.marshal(v, writer);
        return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(writer.toString()))).getDocumentElement();
    }
}
  1. 在Header和Root类的字段上添加适配器注解:
// Header.java
@Data
@XmlRootElement(name = "header")
@XmlAccessorType(XmlAccessType.FIELD)
public class Header<T> {
    @XmlAnyElement
    @XmlJavaTypeAdapter(ApiElementAdapter.class)
    private T headerDetail;
}

// Root.java
@Data
@XmlRootElement(name = "root")
@XmlAccessorType(XmlAccessType.FIELD)
public class Root<T, E> {
    @XmlElement
    private Header<T> header;
    @XmlAnyElement
    @XmlJavaTypeAdapter(ApiElementAdapter.class)
    private E body;
}

该方式灵活性更高,可适配多种不同API元素,通过父节点判断选择正确的反序列化类。

内容的提问来源于stack exchange,提问作者Charles

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 09:54:55