背景

在实际项目开发中,特别是涉及到中文输入输出的时候,大家肯定都被各种乱码问题坑过。如果遇到复杂的系统,为了乱码问题折腾几天也不是不可能。

在最近的项目中,我也遇到了一个头疼的乱码问题。解决完成后,也有了一些心得和体会,总结在这里做为抛砖引玉。

问题描述

在我们这个项目中,主要是通过系统中一系列模块的处理,最终生成结果报告。

项目的总体系统结构如下:

乱码问题就出现在最终的结果报告中,即 “结果报告 2” 中一部分内容出现了中文乱码,另一部分则正常。

问题分析和定位

由于该系统结构比较复杂,接口众多,所以,在项目刚开始开发的时候,为了避免乱码问题,我们决定统一编码格式。我们在项目中约定 Java 代码中凡是涉及到文件的输入输出以及控制台输入输出,所有的编码格式都采用 UTF-8。
既然是这样,为什么还会出现乱码?这里只能从代码和结果着手,一步一步的分析和定位。

由于整个系统模块众多,所以首先我们应该确定问题的边界,即确定问题出现在哪一个模块中。然后,针对具体模块来进一步定位根因。

观察整个系统,核心模块都在 Java 服务中,而 Linuxshell 只是起到连接和调用各个模块的作用,那么我们的重点应该放在 Java 服务中。

1、先看乱码表现出来的地方:乱码出现在结果报告 2 中,但不是所有的中文内容都是乱码,即乱码只出现在第一部分,而第二部分则是正常的。向上追溯这两部分的来源,发现它们主要都是来自于远程服务生成并下载到本地的 “结果报告 1”。

2、既然 “结果报告 2” 的内容主要来源于 “结果报告 1”,那么先检查 “结果报告 1” 是否正常生成。在远程服务上检查 “结果报告 1”,发现文件格式的确是 UTF-8,中文内容也都能正常显示,所以,排除远程服务问题。那么,乱码的产生应该是远程服务生成的文件及其内容在后续传递过程中发生了编码格式的变化。

3、远程服务的 “结果报告 1” 生成以后,会被自动下载到本地,由本地服务处理。理论上来说,文件下载时采用二进制方式下载,不会对文件编码产生影响。检查确认下载后的文件,编码格式也全部都是 UTF-8,中文内容同样也都能正常显示,排除下载过程中的问题。

4、文件下载完成后,Java Service 2 会读取部分文件(这里称之为文件 1)的内容,进行一些加工处理,并传递给 Java Service 3。还有一部分文件(这里称之为文件 2)则直接拷贝文件传递给 Java Service 3。文件拷贝传递时,同样是用二进制方式,不会改变文件编码格式。再次检查确认也证明了这一点没有问题。

5、再检查二者读文件的地方,发现都包含如下代码:
InputStreamReader isr = new InputStreamReader(newFileInputStream(file), "UTF-8");
BufferedReader br= newBufferedReader(isr);

可以看到,二者读文件的代码中都包含了设置 UTF-8 编码格式,那为什么最终一个有乱码而另一个没有?

6、再进一步分析发现,结果报告 2 的第一部分内容由 Java Service 2 从文件 1 中读取后,又经过格式化和处理(不影响编码格式),然后通过 Java Service 3 传递给 Java Service 4。而第二部分内容是直接由 Java Server 4 直接从文件 2 中读取。读文件时,编码格式是没有问题的,那么问题很可能产生在读取的内容从 Java Service 1 传递到 Java Service 4 的过程中。

7、通过如下方式将传递的字符串打印在控制台:
System.out.println( new String(str.getBytes(),"UTF-8"));
在 Java Service 2 和 Java Service 3 中,字符串 str 打印正常,没有出现乱码。而 Java Service 4 中打印接收到的 str 则出现如下内容:
??????????????即,出现了乱码。
8、这时,问题就明确了,乱码是字符串 str 从 Java Service 3 传递到 Java Service 4 时带来的,原因就是传递过程中字符串的编码格式发生了变化。

9、再看 Java Service 3,发现它利用了 Apache 封装的一个 LinuxShell 来调用 Java Service 4,而出现乱码的字符串也是通过这个 Linux Shell 来传递的。

从这里可以看出,问题不是出现在 Java Service 本身,而是出现在消息的传递过程中。

通过阅读 JVM 的文档资料发现,JVM 在启动时会设置一个默认的字符集编码。JVM 默认字符集编码由 file.encoding 参数指定,如果 JVM 的启动参数里没有 file.encoding 参数,则这个字符集编码由系统编码指定。

我们这里通过 Apache 封装的 LinuxShell 调用 Java Service 4 时,并没有传递 file.encoding 参数,而 Java Service 4 是一个被其他进程启动的独立 JVM 和独立进程,这样 Java Service 4 启动后,就会采用系统编码格式,系统编码格式如下:

上面的编码表示为 “C”,这是表示英文 ASCII 的编码格式。也就是说,Java Service 3 中以 “UTF-8” 编码的中文传递到 Java Service 4 以后,Java Service 4 以 “C” 编码来解析,这样肯定是解析不了的,必然出现乱码。

问题解决

问题的根源找到了,那么,我们这里有两种方案来解决:

方案一:在使用 Apache 封装的 LinuxShell 调用

Java Service 4 时,传入 file.encoding=UTF-8 参数来启动 JVM。

方案二:将系统编码修改为 UTF-8。

两个方案均验证通过。

总结

在 Java 项目开发中,编码问题经常涉及到如下 4 个方面:

1、Java 源文件编码

Java 的源文件可以是任何编码的文件,并且,源文件的编码格式不影响最终的运行。但是,如果源文件中包含有中文,则编译时需要指明源文件的编码方式,比如 javac -encoding utf8 HelloWorld.java,如果不指定,默认是操作系统编码。当编译时的编码格式与源文件的编码格式不一致时,很可能出现编译失败问题。

2、Java class 文件编码

无论源文件的编码格式是什么,Java class 文件都是 Unicode 编码(UTF-16)。正因为 class 文件的编码方式统一,所以 class 文件才能够跨平台。

3、JVM 编码

JVM 的编码是 UTF-16,即:双字节表示一个字符。

4、JVM 字符集编码

JVM 字符集编码就是 JVM 在处理输入、输出、字节流等数据时所采用的编码格式,包括文件输入输出、Java 程序运行中的字符串解析等等。

在以上 4 个方面,我们不需要关注 2、3 两点,第 1 点只要能保证源文件编译通过,也无需过多关注。所以,最常见和最重要的是第 4 点,也即我们前文利用 file.encoding 或系统编码所设置的编码,乱码往往是这里的编码出现了问题。

那么,一旦遇到第 4 点的乱码问题,我们应该从哪些方面入手呢?这里,我们可以从如下几个方面去排查:

(1)被 Java 程序读取或写入的文件本身的编码;

(2)Java 程序中对文件的读取、写入时采用的编码;

(3)JVM 的字符集编码;

(4)操作系统的编码。

解决了以上这几个编码的一致性或相容性,Java 程序的乱码问题基本上就解决了。

关注腾讯移动品质中心 TMQ,获取更多测试干货!

版权所属,禁止转载!!!


↙↙↙阅读原文可查看相关链接,并与作者交流