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区分节点位置
- 移除
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); } }
- 修改
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; }
- 初始化
JAXBContext时加入ObjectFactory:
JAXBContext jaxbContext = JAXBContext.newInstance(Root.class, ObjectFactory.class);
通过scope参数,JAXB可区分不同父节点下的同名元素,分别映射到对应类。
方案二:自定义XmlAdapter判断节点位置
- 创建通用适配器,根据元素父节点名称选择反序列化类:
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(); } }
- 在
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
相关产品推荐
相关产品推荐

