zoukankan      html  css  js  c++  java
  • 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>&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"?>
    "
                    + 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的编译限制. 同时能满足之前的功能需求, 值得小小鼓励一下, ^_^.

     
  • 相关阅读:
    PAT顶级 1024 Currency Exchange Centers (35分)(最小生成树)
    Codeforces 1282B2 K for the Price of One (Hard Version)
    1023 Have Fun with Numbers (20)
    1005 Spell It Right (20)
    1092 To Buy or Not to Buy (20)
    1118 Birds in Forest (25)
    1130 Infix Expression (25)
    1085 Perfect Sequence (25)
    1109 Group Photo (25)
    1073 Scientific Notation (20)
  • 原文地址:https://www.cnblogs.com/mumuxinfei/p/9121773.html
Copyright © 2011-2022 走看看