前言:
最近的工做內容跟銀行有些交互, 對方提供的數據格式採用xml(不是預期的json/protobuf). 爲了開發方便, 須要藉助jaxb來實現xml和java對象之間的映射. 它仍是有點像jackson, 經過簡單的註解配置, 就能輕鬆實現json和java對象的互轉. 不過筆者在java類中引入泛型時, 仍是踩了很多jaxb的坑, 這邊作下筆記.java
實現的目標:
交互的數據格式和協議遵循通用的設計, 由header和body構成.
請求的數據格式以下:json
<?xml version="1.0" encoding="UTF-8" ?> <root> <!-- 請求頭 --> <header></header> <request> <!-- 具體的請求參數, 根據接口而定 --> </request> </root>
響應的數據格式以下:app
<?xml version="1.0" encoding="UTF-8" ?> <root> <!-- 響應頭 --> <header></header> <response> <!-- 具體的響應結果, 根據接口而定 --> </response> </root>
header信息頭相對固定, 而具體的request/response取決於具體的業務接口, 在進行對象映射中, 咱們也是針對body消息體進行泛型化.測試
請求類抽象和測試代碼:
針對請求的數據格式, 咱們能夠輕易的設計以下類結構:ui
// *) 請求類(模板) @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的註解.
測試代碼以下:spa
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註解.xml
@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; }
運行測試的結果以下:對象
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類規避該問題.
修改代碼以下:blog
@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)來達到目的, 再次修改版本.
@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>
響應類抽象和測試代碼:
有了請求類的順利結果, 咱們在設計響應類也是有跡可循.
響應類的代碼以下:
@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註解.
測試用例代碼以下:
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註解, 大體以下:
@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), 網友的作法也是相似, 只是沒有給出直接的理由.
@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> { }
修改測試代碼:
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(); } }
運行結果以下:
HelloRes(super=Res(header=header_val, value=HelloAck(key=key_val)))
符合預期, 這邊的作法就是wrap一個泛型類, 姑且能夠理解爲在編譯前指定類, 避免反射出誤差.
總結: 總的來講jaxb在涉及泛型時, 仍是有一些坑的, 這邊總結了一下. 不過總的來講, 知其然不知其因此然, 希翼後面可以對jaxb的底層實現有個深刻的瞭解.