但行好事  莫问前程

Spring解析XML bean文件问题记录

已授权,作者:guimy

在使用Spring解析一个XML bean文件时遇到了编码的问题,报错异常栈如下所示:

Exception in thread "main" org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document from resource loaded from byte array; nested exception is com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: Invalid byte 2 of 2-byte UTF-8 sequence.
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:416)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:342)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:310)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:143)
    at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:109)
    at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:80)
    at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:123)
    at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:422)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:352)
    at me.guimy.XmlApplicationContext.<init>(XmlApplicationContext.java:22)
    at me.guimy.XmlApplicationContext.main(XmlApplicationContext.java:42)
Caused by: com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: Invalid byte 2 of 2-byte UTF-8 sequence.
    at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.invalidByte(UTF8Reader.java:701)
    at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.read(UTF8Reader.java:372)
    at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.load(XMLEntityScanner.java:1895)
    at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.scanData(XMLEntityScanner.java:1375)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanCDATASection(XMLDocumentFragmentScannerImpl.java:1654)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl$FragmentContentDriver.next(XMLDocumentFragmentScannerImpl.java:3014)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:602)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:505)
    at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:841)
    at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:770)
    at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:141)
    at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:243)
    at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:339)
    at org.springframework.beans.factory.xml.DefaultDocumentLoader.loadDocument(DefaultDocumentLoader.java:75)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:396)
    ... 10 more

关键的报错信息时Invalid byte 2 of 2-byte UTF-8 sequence.这个错误,很多文章提到的解决办法是将encoding改为GBK编码或者其他编码,但是为什么呢?产生这个问题的根本原因是什么?为了搞清楚这个问题,我们先简化代码,关键代码如下:

public class XmlApplicationContext extends AbstractXmlApplicationContext {

    private Resource configResource;

    private ClassLoader cl;

    public XmlApplicationContext(String str) {
        configResource = new ByteArrayResource(str.getBytes());
        cl = this.getClassLoader();
        refresh();
    }

    @Override
    protected Resource[] getConfigResources() {
        return new Resource[]{this.configResource};
    }

    public static void main(String[] args) {
        String data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
            + "<!DOCTYPE beans PUBLIC \"-//SPRING//DTD BEAN//EN\" \"http://www.springframework.org/dtd/spring-beans"
            + ".dtd\">\n"
            + "<beans>\n"
            + "  <bean class=\"me.guimy.Student\" id=\"student\">\n"
            + "    <property name=\"name\">\n"
            + "      <value><![CDATA[中文]]></value>\n"
            + "    </property>\n"
            + "  </bean>\n"
            + "</beans>";

        ApplicationContext applicationContext = new XmlApplicationContext(data);
    }
}

这段代码中加载一个SpringXML bean文件,其中有一个值是中文,很显然在解析到中文的时候就报了Invalid byte 2 of 2-byte UTF-8 sequence错。可以看到已经定义了encoding="UTF-8",所以理论上解析在解析的时候不应该无法解析中文的错误。难道是遇到JDKbug了?从异常栈可以看到Spring在解析XML调用了JDK中com.sun.org.apache.xerces.internal.jaxp包下的类来解析,所以针对这个疑问,可以先 debug 到 具体的类中排查到底使用什么编码在解析,其实也可以从异常栈的UTF8Reader可以看到是使用 UTF-8 的编码在解析。通过 debugUTF8Reader可以了解到,这个程序执行过程中解析XML的过程其实是解析的文本对应的字节序列。
既然知道了解析XML文本对应的字节序列是使用的UTF-8编码,并且XML文本中也指定了UTF-8编码,那么怀疑的方向就是字节序列是怎么生成的,是否是UTF-8编码对应的字节序列?所以现在需要确定的是如何将XML文本转换成字节序列的。
可以看看XmlApplicationContext这个类的构造方法。传入的参数是一个字符串,这段代码中str.getBytes()将字符串转换为字节数组使用了默认的编码,再看String.getBytes方法的代码:

public byte[] getBytes() {
    return StringCoding.encode(value, 0, value.length);
}
static byte[] encode(char[] ca, int off, int len) {
    String csn = Charset.defaultCharset().name();
    try {
        return encode(csn, ca, off, len);
    } catch (UnsupportedEncodingException x) {
        warnUnsupportedCharset(csn);
    }
    try {
        return encode("ISO-8859-1", ca, off, len);
    } catch (UnsupportedEncodingException x) {
        System.exit(1);
        return null;
    }
}

String csn = Charset.defaultCharset().name()这行代码可以看到去拿了默认的编码,而默认的编码是通过file.encoding来指定的,所以很简单,打印一下Syste.getProperty("file.encoding")的值,所以执行了一次System.out.println(System.getProperty("file.encoding"))发现打印出来的是GBK。那问题也就明了: 在XML转换成字节序列时,使用GBK编码获得字节徐磊,而在从字节序列转换成字符串时按照UTF-8编码去解析的,这两个编码完全不同,所以报Invalid byte 2 of 2-byte UTF-8 sequence也就不奇怪了。
那么为什么是Invalid byte 2 of 2-byte UTF-8 sequence而不是Invalid byte 2 of 3-byte UTF-8 sequence或者是Invalid byte 3 of 3-byte UTF-8 sequence呢?简单来说GBK 编码的字节序列,在UTF-8编码下解析时,UTF-8识别到根据某一个字节识别到当前连续的两个字节为一个字符,所以再解析第二个字节,但是发现第二个字节不符合UTF-8 二字节字符的第二字节的编码规则,所以就报了Invalid byte 2 of 2-byte UTF-8 sequence
解释的更详细一点,先看看UTF-8的编码规则:

1. 0x00-0x7F 这个范围的 Unicode 字符使用一个字节编码,其最高位为 0;
2. 所有的编码为多字节的的字符编码,非首字节的第一个位为 1,第二位为 0;
3. 0x080-0x7FF 这个范围的 Unicode 字符使用两个字节编码,第一个字节前两位为 1,第三位为0;
4. 0x0800-0xFFFF 这个范围的 Unicode 字符使用三个字节编码,第一个字节的前三位为 1,第四位为 0;
5. 0x010000-0x10FFFF 这个范围的 Unicode 字符使用四个字节编码,第一个字节的前四位为 1,第五位为 0。

对于二字节字符,第一个字节为110xx xxx,第二个字节为10xx xxxx,所以如果检查到某一个字节是10xx xxxx,那么就会去检查第二个字节头两位是否是10,如果不是就认为这不是一个有效的UTF-8字符。比如中文这个词中,中这个字符的GBK编码为1101 0110 1101 0000,通过UTF-8的编码规则解析时,读到第一个字节1101 0110识别到它是一个二字节字符的第一个字节,那么第二个字节就应该是10xx xxxx这样的,但是实际上第二个字节是1101 0000,这个时候就报了这个二字节的UTF-8字符的第二个字节无效,即Invalid byte 2 of 2-byte UTF-8 sequence

打赏
欢迎关注人生设计师的微信公众账号
公众号ID:longjiazuoA

未经允许不得转载:人生设计师 » Spring解析XML bean文件问题记录

分享到:更多 ()

人生设计师-接受不同的声音

联系我关于我