前言

  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>&lt;![CDATA[李雷爱韩梅梅]]&gt;</text>
</root>

  这和我们预期的其实有差异, 我们其实想要的是如下的:

<root>
<text><![CDATA[李雷爱韩梅梅]]></text>
</root>

  本质的原因是Jaxb默认会把字符'<', '>'进行转义, 为了解决这个问题, CharacterEscapeHandler就华丽登场了.

import com.sun.xml.internal.bind.marshaller.CharacterEscapeHandler;

marshaller.setProperty(
"com.sun.xml.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] 程序包com.sun.xml.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[") && text.endsWith("]]>") ) {
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[") && text.endsWith("]]>") ) {
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 = XMLOutputFactory.newInstance()
.createXMLStreamWriter(writer);
// *) 使用动态代理模式, 对streamWriter功能进行干涉调整
XMLStreamWriter cdataStreamWriter = (XMLStreamWriter) Proxy.newProxyInstance(
streamWriter.getClass().getClassLoader(),
streamWriter.getClass().getInterfaces(),
new CDataHandler(streamWriter)
); JAXBContext jc = JAXBContext.newInstance(obj.getClass());
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.marshal(obj, cdataStreamWriter);
return writer.toString(); } 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 = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); StringWriter formattedStringWriter = new StringWriter();
transformer.transform(new StreamSource(new StringReader(xml)),
new StreamResult(formattedStringWriter));
return formattedStringWriter.toString();
} catch (TransformerException e) {
}
return null;
}

  

完整的解决方案

  这边把上述所有的代码完整的贴一遍:

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.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[") && text.endsWith("]]>") ) {
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 = XMLOutputFactory.newInstance()
.createXMLStreamWriter(writer);
// *) 使用动态代理模式, 对streamWriter功能进行干涉调整
XMLStreamWriter cdataStreamWriter = (XMLStreamWriter) Proxy.newProxyInstance(
streamWriter.getClass().getClassLoader(),
streamWriter.getClass().getInterfaces(),
new CDataHandler(streamWriter)
); JAXBContext jc = JAXBContext.newInstance(obj.getClass());
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); marshaller.marshal(obj, cdataStreamWriter);
// *) 对齐差异处理
if ( formatted ) {
return indentFormat(writer.toString());
} else {
return writer.toString();
} } catch (JAXBException e) {
e.printStackTrace();
} catch (XMLStreamException e) {
e.printStackTrace();
}
return null; } // *) xml文本对齐
public static String indentFormat(String xml) {
try {
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer();
// *) 打开对齐开关
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
// *) 忽略掉xml声明头信息
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); StringWriter formattedStringWriter = new StringWriter();
transformer.transform(new StreamSource(new StringReader(xml)),
new StreamResult(formattedStringWriter)); return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ formattedStringWriter.toString();
} 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的编译限制. 同时能满足之前的功能需求, 值得小小鼓励一下, ^_^.

 

Jaxb如何优雅的处理CData的更多相关文章

  1. jaxb生成cdata块

    1.需要添加一个适配器: import javax.xml.bind.annotation.adapters.XmlAdapter; public class AdapterCDATA extends ...

  2. JAXB XML生成CDATA类型的节点

    试了好久才找到一个解决办法,我是用的JAXB的,如果你们也是用JAXB那么可以直接借鉴此方法,别的方式你们自行测试吧 第一步:新增一个适配器类 package com.message.util; im ...

  3. java注解生成xml和包含CDATA问题

    百度java生成xml,有一大推的文章,主要的生成方式一种使用Dom4J ,还有一种使用Jdk自带注解类! 下面主要整理我注解类的使用,(可以参考这篇文章Dom4J生成xml和包含CDATA问题)和x ...

  4. JAXB注解的使用详解

    前言: 最近一直在做各种接口的对接,接触最多的数据类型就是JSON和XML数据,还有XML中包含JSON的数据,而在Java中对象和XML之间的转换经常用到JAXB注解,抽空在这里总结一下,首先做一下 ...

  5. JAXBContext处理CDATA

    今天做Lucene数据源接口时,遇到一个问题,就是输出xml时将某些数据放在CDATA区输出: 1.依赖的jar包,用maven管理项目的话, <dependency> <group ...

  6. XML学习记录1-复习SAX,DOM和JAXB

    对xml文档的解析常见的有JDK中的sax,dom,jaxb,stax和JAVA类库JDOM和DOM4J,下面先说说前三个. Java中解析XML的工具很多,像JDOM,DOM4J等,但Java标准库 ...

  7. XML CData 处理

    调研了 JAXB.XMLMapper(jackson) 具体方式 实现 优势 JAXB 1. 需要增加 CDATA 的Adaptor 2. 需要增加对非CDATA 的 CharacterEscapeH ...

  8. [手把手教程][JavaWeb]优雅的SpringMvc+Mybatis整合之路

    来源于:http://www.jianshu.com/p/5124eef40bf0 [手把手教程][JavaWeb]优雅的SpringMvc+Mybatis整合之路 手把手教你整合最优雅SSM框架:S ...

  9. [Egret]优雅的写http

    首先,自从使用链式调用的写法后,就一发不可收拾的喜爱上了这种优雅的方式.不管是写架构还是写模块,我都会不自觉的使用这种最优雅的方式.链式写法既减少了代码量,又非常优雅的. 在使用 egret 的htt ...

随机推荐

  1. JAVA 两个对象不同为什么他们的hashcode有可能相同

    hashCode是所有java对象的固有方法,如果不重载的话,返回的实际上是该对象在jvm的堆上的内存地址,而不同对象的内存地址肯定不同,所以这个hashCode也就肯定不同了.如果重载了的话,由于采 ...

  2. Pandas初学者代码优化指南

    原文:A Beginner’s Guide to Optimizing Pandas Code for Speed 作者:Sofia Heisler 翻译:无阻我飞扬 摘要:Pandas 是Pytho ...

  3. Java 8 默认方法(Default Methods)

    Java 8 默认方法(Default Methods) Posted by Ebn Zhang on December 20, 2015 Java 8 引入了新的语言特性——默认方法(Default ...

  4. 使用Intellij中的Spring Initializr来快速构建Spring Boot/Cloud工程(十五)

    在之前的所有Spring Boot和Spring Cloud相关博文中,都会涉及Spring Boot工程的创建.而创建的方式多种多样,我们可以通过Maven来手工构建或是通过脚手架等方式快速搭建,也 ...

  5. 整合elk(2)(十三)

    配置.启动kibana 到kibana的安装目录: 1 ./bin/kibana 默认配置即可. 访问localhost:5601,网页显示: 证明启动成功. 创建springboot工程 起步依赖如 ...

  6. 微信订阅号,获取用户openid

    在微信后台,启用服务器配置. 服务器URL地址,要通过Token的验证. private void Auth() { string token = ConfigurationManager.AppSe ...

  7. mysql如何让自增id从某个位置开始设置方法

    一般情况下两种方式: 1.本地数据不需要的情况下直接情况表(尽量不使用

  8. bootstrap 弹框使用

    首先需要准备bootstrap.css,bootstrap .js  jquery 我这里有写好的下载地址如下: https://pan.baidu.com/s/1miMahXe  秘钥:tgts & ...

  9. jenkins部署java项目到远程linux(四)

    (1)新建java项目(maven构建) pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:x ...

  10. asp.net webform 不显示__doPostBack的原因及某种解决方法

    为了维护老旧的asp.net webform系统,今天发现一个问题:asp.net页面没有自动生成__doPostBack方法,提示该js方法不存在.于是在网上找了大量的方法,基本上都说是IE的问题, ...