从Java乱码谈起

背景

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

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

问题描述

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

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

乱码问题就出现在最终的结果报告中,即“结果报告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、通过如下方式将传递的字符串打印在控制台:

在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程序的乱码问题基本上就解决了。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180725G1284C00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券