Jaxb如何优雅的处理CData
前⾔
Jaxb确实是xml和java对象映射互转的⼀⼤利器. 但是在处理CData内容块的时候, 还是有些⼩坑. 结合⽹上搜索的资料, 本⽂提供了⼀种解决的思路, 看看能否优雅地解决CData产出的问题.
常规做法
⽹上最常见的做法是借助XmlAdapter和CharacterEscapeHandler(sun的api)组合来实现.
⾸先定义CDataAdapter类, ⽤于对象类型转换.
public class CDataAdapter extends XmlAdapter<String, String> {
@Override
public String unmarshal(String v) throws Exception {
return v;
}
@Override
public String marshal(String v) throws Exception {
return new StringBuilder("<![CDATA[").append(v).append("]]>").toString();
}
}
其借助注解XmlJavaTypeAdapter作⽤于属性变量上, 如下⾯的类对象上:
@XmlRootElement(name="root")
public static class TNode {
@XmlJavaTypeAdapter(value=CDataAdapter.class)
@XmlElement(name="text", required = true)
private String text;
}
使⽤Marshaller转为xml⽂本的时候, 结果却是如下:
<root>
<text><![CDATA[李雷爱韩梅梅]]></text>
</root>
这和我们预期的其实有差异, 我们其实想要的是如下的:
<root>
<text><![CDATA[李雷爱韩梅梅]]></text>
</root>
本质的原因是Jaxb默认会把字符'<', '>'进⾏转义, 为了解决这个问题, CharacterEscapeHandler就华丽登场了.
import l.internal.bind.marshaller.CharacterEscapeHandler;
新手上路须知marshaller.setProperty(
"l.internal.bind.marshaller.CharacterEscapeHandler",
new CharacterEscapeHandler() {
@Override
public void escape(char[] ch, int start, int length, boolean isAttVal, Writer writer)
throws IOException {
writer.write(ch, start, length);
}
}
);
测试结果, 完美地解决问题. 然后随之⽽来的问题, 稍有些尴尬, 使⽤maven进⾏编译打包的时候, 会遇到如下错误:
[ERROR] Compilation failure
[ERROR] 程序包l.internal.bind.marshaller不存在
Java⼯程开发, ⼀般不建议直接调⽤内部的api(以com.sun开头).
改进⽅案:
参考了不少⽹友的博⽂, ⼤致思路都是⼀样的, 就是借助重载XMLStreamWriter类实现. 更确实的做法是重载writeCharacters⽅法, 在遇到CData标记(<![CDATA[]]>)包围的⽂本时, 选择调⽤writeCData函数, 可⽤以下代码来⼤致说明:
public class CDataXMLStreamWriter implements XMLStreamWriter {
// *) 重载writeCharacters, 遇CDATA标记, 则转⽽调⽤writeCData⽅法
@Override
public void writeCharacters(String text) throws XMLStreamException {
if ( text.startsWith("<![CDATA[") && dsWith("]]>") ) {
是谁家的姑娘是什么歌writeCData(text.substring(9, text.length() - 3));
} else {
writeCharacters(text);
}
}
// *) 演⽰使⽤
}
真实的做法, 不会采⽤完整的去实现XmlStreamWriter接⼝的⽅案, ⽽是采⽤代理模式.这边采⽤动态代理的⽅法.
private static class CDataHandler implements InvocationHandler {
// *) 单独拦截 writeCharacters(String)⽅法
private static Method gWriteCharactersMethod = null;
static {
try {
gWriteCharactersMethod = XMLStreamWriter.class
.getDeclaredMethod("writeCharacters", String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
private XMLStreamWriter writer;
public CDataHandler(XMLStreamWriter writer) {
this.writer = writer;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ( gWriteCharactersMethod.equals(method) ) {
String text = (String)args[0];
// *) 遇到CDATA标记时, 则转⽽调⽤writeCData⽅法
if ( text != null && text.startsWith("<![CDATA[") && dsWith("]]>") ) {
writer.writeCData(text.substring(9, text.length() - 3));
return null;
}
}
return method.invoke(writer, args);
}
}
具体的Marshaller代码⽚段如下所⽰:
public static <T> String mapToXmlWithCData(T obj) {
try {
StringWriter writer = new StringWriter();
XMLStreamWriter streamWriter = wInstance()
.createXMLStreamWriter(writer);
// *) 使⽤动态代理模式, 对streamWriter功能进⾏⼲涉调整
XMLStreamWriter cdataStreamWriter = (XMLStreamWriter) wProxyInstance(
new CDataHandler(streamWriter)
);
JAXBContext jc = Class());
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
marshaller.marshal(obj, cdataStreamWriter);
String();
} catch (JAXBException e) {
e.printStackTrace();
} catch (XMLStreamException e) {
e.printStackTrace();
什么风凛冽}
return null;
}
测试的结果, 完美地解决了CData的问题(功能实现+绕过sun api), 不过这⾥⾯还有点⼩瑕疵, 就是对齐问题, 这段代码没法控制对齐.
对齐改进
这边需要借助Transformer类实现, 思路是对最终的xml⽂本进⾏格式化处理. // *) 对xml⽂本进⾏格式化转化
public static String indentFormat(String xml) {
try {
TransformerFactory factory = wInstance();
Transformer transformer = wTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{/xslt}indent-amount", "4");
StringWriter formattedStringWriter = new StringWriter();
new StreamResult(formattedStringWriter));
String();
} catch (TransformerException e) {
}
return null;
}
完整的解决⽅案
这边把上述所有的代码完整的贴⼀遍:
l.stream.XMLOutputFactory;
l.stream.XMLStreamException;
l.stream.XMLStreamWriter;
l.transform.OutputKeys;
l.transform.Transformer;
l.transform.TransformerException;
l.transform.TransformerFactory;
l.transform.stream.StreamResult;
l.transform.stream.StreamSource;
// *) XmlAdapter类, 修饰类字段, 达到⾃动添加CDATA标记的⽬标
public static class CDataAdapter extends XmlAdapter<String, String> {
@Override
public String unmarshal(String v) throws Exception {
return v;
}
@Override
public String marshal(String v) throws Exception {
return new StringBuilder("<![CDATA[").append(v).append("]]>")
.
toString();
}
}
// *) 动态代理
private static class CDataHandler implements InvocationHandler {
private static Method gWriteCharactersMethod = null;
static {
try {
gWriteCharactersMethod = XMLStreamWriter.class
.getDeclaredMethod("writeCharacters", String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
private XMLStreamWriter writer;
public CDataHandler(XMLStreamWriter writer) {
this.writer = writer;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ( gWriteCharactersMethod.equals(method) ) {
String text = (String)args[0];
if ( text != null && text.startsWith("<![CDATA[") && dsWith("]]>") ) {
writer.writeCData(text.substring(9, text.length() - 3));
return null;
}
}
return method.invoke(writer, args);
}
}
如何做手撕包菜// *) ⽣成xml
public static <T> String mapToXmlWithCData(T obj, boolean formatted) {
try {
StringWriter writer = new StringWriter();
XMLStreamWriter streamWriter = wInstance()
.createXMLStreamWriter(writer);
// *) 使⽤动态代理模式, 对streamWriter功能进⾏⼲涉调整
XMLStreamWriter cdataStreamWriter = (XMLStreamWriter) wProxyInstance( Class().getClassLoader(),
new CDataHandler(streamWriter)
);
JAXBContext jc = Class());
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
marshaller.marshal(obj, cdataStreamWriter);
// *) 对齐差异处理
if ( formatted ) {
return String());
} else {
String();
}
} catch (JAXBException e) {
e.printStackTrace();
} catch (XMLStreamException e) {
e.printStackTrace();
}
return null;
}
// *) xml⽂本对齐
public static String indentFormat(String xml) {
try {
TransformerFactory factory = wInstance();
Transformer transformer = wTransformer();
// *) 打开对齐开关
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
// *) 忽略掉xml声明头信息
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty("{/xslt}indent-amount", "4");
StringWriter formattedStringWriter = new StringWriter();中秋节短信
new StreamResult(formattedStringWriter));
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ String();
} catch (TransformerException e) {
}
return null;
}
编写具体的测试案例:
@NoArgsConstructor
@AllArgsConstructor
@XmlRootElement(name="root")
public static class TNode {
@XmlElement(name="key", required = true)
private String key;
@XmlJavaTypeAdapter(value=CDataAdapter.class)
@XmlElement(name="text", required = true)
private String text;
关于郑成功的故事
}
public static void main(String[] args) {
TNode node = new TNode("key", "李雷爱韩梅梅");
String xml = mapToXmlWithCData(node, true);
System.out.println(xml);
}
测试输出的结果如下:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<key>key</key>
<text><![CDATA[李雷爱韩梅梅]]></text>
</root>
总结
总的来说, 改进的⽅案规避了sun api的编译限制. 同时能满⾜之前的功能需求, 值得⼩⼩⿎励⼀下, ^_^.
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论