在公司项目的中间件代码里看到有些配置文件里有很多 "\uXXXX"
标记的 unicode 字符,其实就是配置里的中文字符。我一时不得其解,开发平台是 Linux,项目文件都是 UTF-8 编码,配置文件里的中文字符为什么还会被转码?
编码那些事儿
Spring 读取 .properties
文件并将配置内容加载进 Properties 类,文档中明确写明
… the input/output stream is encoded in ISO 8859-1 character encoding. Characters that cannot be directly represented in this encoding can be written using Unicode escapes as defined in section 3.3 of The Java™ Language Specification; only a single ‘u’ character is allowed in an escape sequence. The native2ascii tool can be used to convert property files to and from other character encodings.
Java 的 I/O 流是由 ISO 8859-1 编码的,如果要让 Java I/O 读写 ISO 8859-1 标准以外的字符,就需要把这些字符用 Unicode 编码。也就是转码成上述的 "\uXXXX"
形式。JDK 提供了转码工具 ../bin/native2ascii
。比如项目里需要配置包含中文的属性值,就只能把中文内容转码成 Unicode 编码。
native2ascii –encoding UTF-8 foo_utf8.properties foo.properties
上面的 foo_utf8.properties 是由 UTF-8 编码保存的配置文件,foo.properties 是将非 ISO 8859-1 字符转码为 Unicode 后的配置文件,也就是提供给 Spring 解析的配置文件。
// foo_utf8.properties
city=北京
company=京东
// foo.properties
city=\u5317\u4eac
company=\u4eac\u4e1c
Java 读取 UTF-8
虽然明白了这其中的蹊跷,但是包含中文的配置文件一定得转码吗,仔细阅读 Properties Class 的源码后,还是有更优雅的解决方案,原来 Sun 公司都已经帮我们设计好了。 Properties 类调用 load() 方法加载配置文件:
// Properties Class source code
package java.util;
public synchronized void load(Reader reader) throws IOException {
load0(new LineReader(reader));
}
public synchronized void load(InputStream inStream) throws IOException {
load0(new LineReader(inStream));
}
private void load0 (LineReader lr) throws IOException {
char[] convtBuf = new char[1024];
int limit;
int keyLen;
int valueStart;
char c;
boolean hasSep;
boolean precedingBackslash;
while ((limit = lr.readLine()) >= 0) {
c = 0;
keyLen = 0;
valueStart = limit;
hasSep = false;
//System.out.println("line=<" + new String(lineBuf, 0, limit) + ">");
precedingBackslash = false;
while (keyLen < limit) {
c = lr.lineBuf[keyLen];
//need check if escaped.
if ((c == '=' || c == ':') && !precedingBackslash) {
valueStart = keyLen + 1;
hasSep = true;
break;
} else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) {
valueStart = keyLen + 1;
break;
}
if (c == '\\') {
precedingBackslash = !precedingBackslash;
} else {
precedingBackslash = false;
}
keyLen++;
}
while (valueStart < limit) {
c = lr.lineBuf[valueStart];
if (c != ' ' && c != '\t' && c != '\f') {
if (!hasSep && (c == '=' || c == ':')) {
hasSep = true;
} else {
break;
}
}
valueStart++;
}
String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf);
String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf);
put(key, value);
}
}
private String loadConvert (char[] in, int off, int len, char[] convtBuf) {
if (convtBuf.length < len) {
int newLen = len * 2;
if (newLen < 0) {
newLen = Integer.MAX_VALUE;
}
convtBuf = new char[newLen];
}
char aChar;
char[] out = convtBuf;
int outLen = 0;
int end = off + len;
while (off < end) {
aChar = in[off++];
if (aChar == '\\') {
aChar = in[off++];
if(aChar == 'u') {
// Read the xxxx
int value=0;
for (int i=0; i<4; i++) {
aChar = in[off++];
switch (aChar) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
value = (value << 4) + aChar - '0';
break;
case 'a': case 'b': case 'c':
case 'd': case 'e': case 'f':
value = (value << 4) + 10 + aChar - 'a';
break;
case 'A': case 'B': case 'C':
case 'D': case 'E': case 'F':
value = (value << 4) + 10 + aChar - 'A';
break;
default:
throw new IllegalArgumentException(
"Malformed \\uxxxx encoding.");
}
}
out[outLen++] = (char)value;
} else {
if (aChar == 't') aChar = '\t';
else if (aChar == 'r') aChar = '\r';
else if (aChar == 'n') aChar = '\n';
else if (aChar == 'f') aChar = '\f';
out[outLen++] = aChar;
}
} else {
out[outLen++] = aChar;
}
}
return new String (out, 0, outLen);
}
我从 Properties Class 的源码里抄录了加载配置文件的关键代码。不难读懂代码的含义,简单说明下,load() 方法是通过调用 load0() 方法来按行读取配置文件,并装配成 key - value 的键值对,在 load0() 内部调用了 loadConvert() 方法,用来将 Unicode 字符转换成原始的格式。其中,load() 方法被重载实现,可以传入 Reader 参数,或者 InputStream 参数。
这里就涉及到Java I/O 里 Reader/Writer 和 InputStream/OutputStream 的差异了,Reader 类用于读取文本数据(char、String 流),InputStream 类则用于读取二进制数据(byte 流)。Reader 类可以指定 Charset 参数来设置编码格式,如 UTF-8 等。因此只需要指定 Reader 对象的 charset,就可以无痛解析带中文的配置文件了,只是有一点要指出, FileReader 的构造函数是假定使用的编码格式是正确的(即默认的ISO 8859-1),不支持指定文件编码格式,因此还是需要借助 InputStream 类。示例代码如下,很简单:
// ReadUTF8Props.java
Properties properties = new Properties();
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("foo_utf8.properties");
try {
InputStreamReader isr = new InputStreamReader(inputStream, "UTF-8");
try {
properties.load(isr);
} finally {
isr.close();
}
} finally {
inputStream.close();
}
到这儿,总算对这个问题有了一点点深入的认识。