Invalid byte 2 of 2-byte UTF-8 sequence
前几天在使用 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 的编码规则。
- 0x00-0x7F 这个范围的 Unicode 字符使用一个字节编码,其最高位为 0;
- 所有的编码为多字节的的字符编码,非首字节的第一个位为 1,第二位为 0;
- 0x080-0x7FF 这个范围的 Unicode 字符使用两个字节编码,第一个字节前两位为 1,第三位为0;
- 0x0800-0xFFFF 这个范围的 Unicode 字符使用三个字节编码,第一个字节的前三位为 1,第四位为 0;
- 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