前言:
最近的工作內容跟銀行有些交互, 對方提供的數據格式采用xml(不是預期的json/protobuf). 為了開發方便, 需要借助jaxb來實現xml和java對象之間的映射. 它還是有點像jackson, 通過簡單的注解配置, 就能輕松實現json和java對象的互轉. 不過筆者在java類中引入泛型時, 還是踩了不少jaxb的坑, 這邊做下筆記.
實現的目標:
交互的數據格式和協議遵循通用的設計, 由header和body構成.
請求的數據格式如下:
<?xml version="1.0" encoding="UTF-8" ?>
<root>
<!-- 請求頭 -->
<header></header>
<request>
<!-- 具體的請求參數, 根據接口而定 -->
</request>
</root>
響應的數據格式如下:
<?xml version="1.0" encoding="UTF-8" ?>
<root>
<!-- 響應頭 -->
<header></header>
<response>
<!-- 具體的響應結果, 根據接口而定 -->
</response>
</root>
header信息頭相對固定, 而具體的request/response取決於具體的業務接口, 在進行對象映射中, 我們也是針對body消息體進行泛型化.
請求類抽象和測試代碼:
針對請求的數據格式, 我們可以輕易的設計如下類結構:
// *) 請求類(模板)
@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的注解.
測試代碼如下:
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注解.
@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類規避該問題.
修改代碼如下:
@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的底層實現有個深入的了解.
最后附上一個工具類
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.*;
/**
* @author hp
* @date 2020/2/13
*/
public class XmlUtils {
private static String DEFAULT_CHARSET = "Unicode";
public static String toXml(Object model) throws JAXBException, IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
marshal(model, output);
output.flush();
return new String(output.toByteArray(), DEFAULT_CHARSET);
}
public static String toXml(Object model, boolean isFormatOut) throws JAXBException, IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
marshal(model, output, isFormatOut);
output.flush();
return new String(output.toByteArray(), DEFAULT_CHARSET);
}
public static void marshal(Object model, OutputStream output) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(model.getClass());
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
jaxbMarshaller.setProperty(Marshaller.JAXB_ENCODING, DEFAULT_CHARSET);
jaxbMarshaller.marshal(model, output);
}
public static void marshal(Object model, OutputStream output, boolean isFormatOut) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(model.getClass());
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, isFormatOut);
jaxbMarshaller.setProperty(Marshaller.JAXB_ENCODING, DEFAULT_CHARSET);
jaxbMarshaller.marshal(model, output);
}
public static <T> T parseXml(Class<T> clazz, String xml) throws JAXBException, IOException {
byte[] buf = xml.getBytes(DEFAULT_CHARSET);
ByteArrayInputStream input = new ByteArrayInputStream(buf, 0, buf.length);
return unmarshal(clazz, input);
}
@SuppressWarnings("unchecked")
public static <T> T unmarshal(Class<T> clazz, InputStream input) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
return (T) jaxbUnmarshaller.unmarshal(input);
}
public static void saveXmlToFile(Object model, String filename) throws FileNotFoundException, JAXBException {
FileOutputStream fos = new FileOutputStream(filename);
marshal(model, fos);
}
public static void saveXmlToFile(Object model, File file) throws FileNotFoundException, JAXBException {
FileOutputStream fos = new FileOutputStream(file);
marshal(model, fos);
}
public static <T> T loadXmlFromFile(Class<T> clazz, String filename) throws FileNotFoundException, JAXBException {
return unmarshal(clazz, new FileInputStream(filename));
}
public static <T> T loadXmlFromFile(Class<T> clazz, File file) throws FileNotFoundException, JAXBException {
return unmarshal(clazz, new FileInputStream(file));
}
public static <T> T loadXmlFromFile(Class<T> clazz, InputStream is) throws JAXBException {
return unmarshal(clazz, is);
}
}