当Jaxb遇到泛型
前言:
最近的工作内容跟银行有些交互, 对方提供的数据格式采用xml(不是预期的json/protobuf). 为了开发方便, 需要借助jaxb来实现xml和java对象之间的映射. 它还是有点像jackson, 通过简单的注解配置, 就能轻松实现json和java对象的互转. 不过笔者在java类中引入泛型时, 还是踩了不少jaxb的坑, 这边做下笔记.
实现的目标:
交互的数据格式和协议遵循通用的设计, 由header和body构成.
请求的数据格式如下:
1 2 3 4 5 6 7 8 | <?xml version= "1.0" encoding= "UTF-8" ?> <root> <!-- 请求头 --> <header></header> <request> <!-- 具体的请求参数, 根据接口而定 --> </request> </root> |
响应的数据格式如下:
1 2 3 4 5 6 7 8 | <?xml version= "1.0" encoding= "UTF-8" ?> <root> <!-- 响应头 --> <header></header> <response> <!-- 具体的响应结果, 根据接口而定 --> </response> </root> |
header信息头相对固定, 而具体的request/response取决于具体的业务接口, 在进行对象映射中, 我们也是针对body消息体进行泛型化.
请求类抽象和测试代码:
针对请求的数据格式, 我们可以轻易的设计如下类结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // *) 请求类(模板) @Getter @Setter @ToString public class Req<T> { private String header; private T value; } // *) 具体的实体请求 @Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class EchoBody { private String key; } |
注: 这边的注解Getter/Setter/ToString等皆是lombok的注解.
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public static void main(String[] args) { Req<EchoBody> req = new Req<EchoBody>(); req.setHeader( "header" ); req.setValue( new EchoBody( "key" )); try { StringWriter sw = new StringWriter(); JAXBContext context = JAXBContext.newInstance(req.getClass()); Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true ); marshaller.marshal(req, sw); System.out.println(sw.toString()); } catch (JAXBException e) { e.printStackTrace(); } } |
注: 该代码主要测试对象到xml的转换是否顺利.
演进和迭代:
先来看第一版本, 引入jaxb注解, 同时省略lombok注解.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @XmlRootElement (name= "root" ) @XmlAccessorType (XmlAccessType.FIELD) public class Req<T> { @XmlElement (name= "header" ,required = true ) private String header; @XmlElement (name= "request" , required = true ) private T value; } @XmlRootElement (name= "request" ) @XmlAccessorType (XmlAccessType.FIELD) public class EchoBody { @XmlElement (name= "key" , required = true ) private String key; } |
运行测试的结果如下:
1 2 3 4 5 6 7 8 | javax.xml.bind.MarshalException - with linked exception: [com.sun.istack.internal.SAXException2: class com.test.Test$EchoBody以及其任何超类对此上下文都是未知的。 javax.xml.bind.JAXBException: class com.test.Test$EchoBody以及其任何超类对此上下文都是未知的。] at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java: 311 ) at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java: 236 ) at javax.xml.bind.helpers.AbstractMarshallerImpl.marshal(AbstractMarshallerImpl.java: 116 ) at com.test.Test.main(Test.java: 55 ) |
来首战遇到一些小挫折, 通过百度得知需要借助@XmlSeeAlso类规避该问题.
修改代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @XmlRootElement (name= "root" ) @XmlAccessorType (XmlAccessType.FIELD) @XmlSeeAlso ({EchoBody. class }) public class Req<T> { @XmlElement (name= "header" ,required = true ) private String header; @XmlElement (name= "request" , required = true ) private T value; } @XmlRootElement (name= "request" ) @XmlAccessorType (XmlAccessType.FIELD) public class EchoBody { @XmlElement (name= "key" , required = true ) private String key; } |
运行后的输出结果如下:
<?xml version= "1.0" encoding= "UTF-8" standalone= "yes" ?> <root> <header>header</header> <request xsi:type= "echoBody" xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" > <key>key</key> </request> </root> |
看来非常的成功, 但是request标签里包含了xsi:type和xmlns:xsi这些属性, 能否把这些信息去除, 网上查阅得知, 借助@XmlAnyElement(lax = true)来达到目的, 再次修改版本.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @XmlRootElement (name= "root" ) @XmlAccessorType (XmlAccessType.FIELD) @XmlSeeAlso ({EchoBody. class }) public class Req<T> { @XmlElement (name= "header" ,required = true ) private String header; @XmlAnyElement (lax = true ) private T value; } @XmlRootElement (name= "request" ) @XmlAccessorType (XmlAccessType.FIELD) public class EchoBody { @XmlElement (name= "key" , required = true ) private String key; } |
这次的结果可以称得上完美(perfect):
<?xml version= "1.0" encoding= "UTF-8" standalone= "yes" ?> <root> <header>header</header> <request> <key>key</key> </request> </root> |
响应类抽象和测试代码:
有了请求类的顺利结果, 我们在设计响应类也是有迹可循.
响应类的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @Getter @Setter @ToString public class Res<T> { private String header; private T value; } @Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class EchoAck { private String value; } @Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor public class HelloAck { private String key; } |
注: 这边暂时隐去jaxb的注解, 剩下的都是lombok注解.
测试用例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public static void main(String[] args) { String xml = "" + "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + "<root>\n" + "\t<header>header_val</header>\n" + "\t<response>\n" + "\t\t<key>key_val</key>\n" + "\t</response>\n" + "</root>" ; Res<HelloAck> res = new Res<HelloAck>(); try { JAXBContext jc = JAXBContext.newInstance(res.getClass()); Unmarshaller unmar = jc.createUnmarshaller(); Res<HelloAck> r = (Res<HelloAck>)unmar.unmarshal( new StringReader(xml)); System.out.println(r); } catch (JAXBException e) { e.printStackTrace(); } } |
演进和迭代:
添加jaxb注解, 隐去lombok注解, 大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @XmlRootElement (name= "root" ) @XmlAccessorType (XmlAccessType.FIELD) @XmlSeeAlso ({HelloAck. class , EchoAck. class }) public class Res<T> { @XmlElement (name= "header" ,required = true ) private String header; @XmlAnyElement (lax = true ) private T value; } @XmlRootElement (name= "response" ) @XmlAccessorType (XmlAccessType.FIELD) public class EchoAck { @XmlElement (name= "value" , required = true ) private String value; } @XmlRootElement (name= "response" ) @XmlAccessorType (XmlAccessType.FIELD) public class HelloAck { @XmlElement (name= "key" , required = true ) private String key; } |
运行的如下:
Res(header=header_val, value=EchoAck(value= null )) |
这边需要的注意的是, 代码中指定反解的类是HelloAck, 但是这边反解的类却是EchoAck. 由此可见, jaxb在xml到对象转换时, 其泛型类的选取存在问题(猜测java泛型在编译时类型被擦去, 反射不能确定具体那个类).
针对这种情况, 一个好的建议是, 单独引入实体类(wrapper), 网友的做法也是类似, 只是没有给出直接的理由.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | @Getter @Setter @ToString @XmlTransient // 抽象基类改为注解XmlTransient, 切记 @XmlAccessorType (XmlAccessType.FIELD) public abstract class Res<T> { @XmlElement (name= "header" ,required = true ) private String header; @XmlAnyElement (lax = true ) private T value; } @Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor @XmlRootElement (name= "response" ) @XmlAccessorType (XmlAccessType.FIELD) public class EchoAck { @XmlElement (name= "value" , required = true ) private String value; } @Getter @Setter @ToString @AllArgsConstructor @NoArgsConstructor @XmlRootElement (name= "response" ) @XmlAccessorType (XmlAccessType.FIELD) public class HelloAck { @XmlElement (name= "key" , required = true ) private String key; } @Getter @Setter @ToString (callSuper = true ) @XmlRootElement (name= "root" ) @XmlAccessorType (XmlAccessType.FIELD) @XmlSeeAlso ({HelloAck. class }) public class HelloRes extends Res<HelloAck> { } |
修改测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public static void main(String[] args) { String xml = "" + "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + "<root>\n" + "\t<header>header_val</header>\n" + "\t<response>\n" + "\t\t<key>key_val</key>\n" + "\t</response>\n" + "</root>" ; HelloRes res = new HelloRes(); try { JAXBContext jc = JAXBContext.newInstance(HelloRes. class ); Unmarshaller unmar = jc.createUnmarshaller(); HelloRes r = (HelloRes)unmar.unmarshal( new StringReader(xml)); System.out.println(r); } catch (JAXBException e) { e.printStackTrace(); } } |
运行结果如下:
1 | HelloRes( super =Res(header=header_val, value=HelloAck(key=key_val))) |
符合预期, 这边的做法就是wrap一个泛型类, 姑且可以理解为在编译前指定类, 避免反射出偏差.
总结:
总的来说jaxb在涉及泛型时, 还是有一些坑的, 这边总结了一下. 不过总的来说, 知其然不知其所以然, 希翼后面能够对jaxb的底层实现有个深入的了解.
posted on 2018-04-26 14:25 mumuxinfei 阅读(8076) 评论(3) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构