前几天在使用 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);
    }
}

这段代码中加载一个 Spring 的 XML bean 文件,其中有一个值是中文,很显然在解析到中文的时候就报了Invalid byte 2 of 2-byte UTF-8 sequence.错。可以看到已经定义了 encoding="UTF-8",所以理论上解析在解析的时候不应该无法解析中文的错误。难道是遇到 JDK 的 bug 了?从异常栈可以看到 Spring 在解析 XML 调用了 JDK 中com.sun.org.apache.xerces.internal.jaxp包下的类来解析,所以针对这个疑问,可以先 debug 到 具体的类中排查到底使用什么编码在解析,其实也可以从异常栈的UTF8Reader可以看到是使用 UTF-8 的编码在解析。通过 debug 到UTF8Reader可以了解到,这个程序执行过程中解析 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。

对于二字节字符,第一个字节为110x xxxx,第二个字节为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

Reference

http://guimy.me/%E5%AD%97%E7%AC%A6%E9%9B%86%E4%B8%8E%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81/2017/04/09/charset_and_charencoding.html