目标
了解乱码的成因
了解乱码的定位方式和解决方法
为什么需要编码呢?
因为字符串是需要编码成字节数组作为载体的来存储和传输.
为什么会乱码?
乱码产生的原因一般是因为编码转换出错. 字符串常见编码有GBK和UTF-8等. 如果一个字符串的编码和解码方式不一样, 就会出现乱码.
例如是通过UTF-8编码的, 但通过GBK来解码, 就会变成下面的样子.
字节数组: [-28, -67, -96, -27, -91, -67]
UTF-8解码后: 你好
GBK解码后: 浣犲ソ
如果是通过GBK编码, 但通过UTF-8解码, 就会变成下面的样子.
UTF-8解码后: ���
GBK解码后: 你好
上面是常见的乱码, 可以记住乱码表现形式, 如果是类似的乱码, 就可以大概知道是什么编码问题了.
如何模拟乱码
如果让你写一个java程序, 模拟乱码的情况, 你会怎么写?
java程序模拟乱码
下面这么写会不会有问题, 在编辑器 (例如 IDEA) 里面的控制台看到的是 "浣犲ソ"吗?
public static void main(String[] args) throws IOException {
System.out.println(new String("你好".getBytes("UTF-8"), "GBK"));
}
答案是 不一定.
程序编解码分析
你在控制台看到的字符串经过层层转换最终才能呈现结果, 下面5部分都会对字符串的呈现产生影响:
- .class 文件的编码
- getBytes("UTF-8") 进行转码获取对应编码的字节数组
- new String(,"GBK") 进行解码用于显示字符串
- System.out.println 编码后转成字节流写入控制台
.class 文件的编码
.class 编码默认是UTF-8的. 但 .java 不一定. jdk编译器会把 .java 转成 .class. 意味着.java的编码和编译器程序的解码必须是一致的, (IDEA修改编译编码的方式在备注1)
否则会出现下面的情况, 虽然 .java里面显示的是 "你好", 但实际上变量的内容是 "浣犲ソ"
!
看着是编译器是没问题的, 但实际上运行起来确是乱码, 证明 .class出现问题了
class 里面默认是通过UTF-8编码
getBytes("UTF-8")
只有当class类编码为UTF-8时, 才能拿到正确的字节数组, 否则编码对不上拿到的字节数组就会有问题.
new String(,"GBK")
通过UTF-8编码之后, 通过GBK解码即可模拟出乱码的情况.
System.out.println
, 程序会获取到 控制台的输出流, 并往里面写字节流, 这时候又需要再转一次编码.
控制台
- 控制台应用获取到字节流, 然后通过解码展示在控制台应用上.
编解码总结
上面每个地方都有可能会编码产生影响, 在这个程序里面无法得知1,4,5 的编码到底有没有问题, 所以无法知道控制台输出的结果是什么. 看似转来转去很复杂, 实际上只需要清楚3点即可.
- 字符串会被编码成字节来存储和传输, 字节是没有乱码. 你看到的中文或者乱码都是通过解码得来的(包括你在编辑器看到的中文).
- 在字符串编码之后的字节, 要采用相同的解码方式才不会乱码.
编码的地方有很多, 例如存储和传输, 例如输出到文件(.class), 输出到控制台, String.getBytes() 等等. 解码的地方则例如编辑器看到的中文, 控制台的中文, new String() 其实都在解码.
一般会有三个地方会影响中文的正常呈现,
- 一个是输入, 例如 .class文件, socket
- 一个是处理, 也就是内部的转换, 例如String.getBytes() 或者 使用ByteArrayStream自己转了一下 .
- 一个是输出, 也就是前面提到的解码.
IDEA 使用gradle时控制台乱码
最近发现一个IDEA里面使用gradle插件的一个乱码问题. 下面是特定搞出来的异常, 是编译错误的异常.
查了很多资料, 通过 IDEA64.exe.vmoptions 里面增加 -Dfile.encoding=UTF-8 可以解决问题. 但发现修改编码后控制台的显示也会有变, 所以有没有更好的方式呢, 乱码的原因是什么呢? 我能不能修改gradle的编码方式, 和IDEA保持一致, 就不需要修改 -Dfile.encoding 了.
下面的分析方法可能会有点笨, 但如果都搞懂了对乱码的原因会有更深的理解.
IDEA 涉及到 gradle 的逻辑
组成部分
在 IDEA 里面 gradle 从运行到展示由三部分组成:
- gradle
- Gradle Plugin (gradle的IDEA插件)
执行逻辑
他们的执行逻辑如下:
- Gradle Plugin 首先会通过Process 执行 java gradle-launcher.jar 启动 gradle的deamon 进程.
- gradle进程会启动一个端口用于执行真正的gradle指令和输出指令结果, 因此 Gradle Plugin 会找到 deamon开放的端口进行connect, 并传入gradle指令.
- Gradle Deamon 是一个独立的进程, 被启动后会执行gradle指令内容, 例如编译, 执行等, 通过socket 来返回异常信息. socket的输出流经过转码呈现在 IDEA 的ConsoleView 上面.
通讯方式
乱码分析
乱码只会在编解码的地方出现, 因此一开始需要先找到存在编解码的地方, 然后再逐个进行分析.
可能存在编解码的地方
根据上面的流程可以看到, 中文的源头应该是在gradle deamon, 因为是gradle deamon负责执行gradle指令的, 我们可以推测出可能存在编码和解码的方式有哪些
- JDK JavaCompile 编译产生的异常信息
- Gradle Deamon 接收异常信息
- gradle deamon-> Gradle Plugin
- Gradle Plugin -> IDEA console
逐个进行编解码分析
1. 异常源头
我们先看这个异常信息是哪里来的. 通过对gradle的debug, 发现异常信息是gradle直接调用 JavacTaskImpl 触发编译过程, 然后jdk通过流的方式把异常输出出来. , jdk 里面的多语言使用的是 native 的编码方式, jdk内部的逻辑肯定是指定了这种解码方式的. 所以异常信息的解码一般不会有问题.
2. 异常输出
JavaCompile 通过流的方式输出, Gradle Deamon 通过流的方式写入.
下面的框是 JavaCompile 输出流 , 上面的框是 Gradle Deamon 输入流
JavaCompile 输出流
JDK 通过字节流的方式返回编译异常信息, 并使用 Charset.defaultCharset() 来作为编码
Gradle Deamon 输入流
Gradle Deamon 通过 buffer 接受字节流, 然后同样通过 Charset.defaultCharset() 来作为解码
写和读都是使用 Charset.defaultCharset() , 所以不会乱码.
2. socket 通讯
Gradle Deamon 的写入
Gradle Deamon 通过读出 javaCompile 的输出流拿到异常的信息, 这时候要通过 socket 传给 Gradle Plugin了. socket 的序列化方式是通过 kryo 来序列化的, 但在序列化的时候默认使用了 UTF-8 的形式进行编码 (writeUtf8), 而非 Charset.defaultCharset() .
Gradle Plugin 的写出
写完就是 Gradle Plugin 来读写入的信息了, 这里是对 Gradle Plugin 进行 debug 的截图. 因为也是默认使用UTF-8来解码, 所以也没有问题.
类名为: com.esotericsoftware.kryo.io.Input
3. 控制台交互
debug了一下Gradle Plugin, 在 ConsoleView 这个类中发现了问题. 读还好好的, 怎么在ConsoleView就乱码了.
com.intellij.execution.impl.ConsoleViewImpl
顺着调用链找到正常中文和乱码的中间地带, 发现有个OutputStreamWriter
为什么中间还要再编码解码一次呢?
因为 gradle 是一个脚本, 因此输入输出都是默认使用流的方式. 按照一般的用法, 会通过命令行去触发指令, 再把输出流写入到控制台上的. 但Gradle Plugin 刚好是通过自己 connect 的方式而非再起一个进程被动触发, 因此输入输出都在同一个进程里面, 但还是要通过流的方式去获取输出.
OutputStreamWriter 的编码方式是上文提到的 Charset.defaultCharset() , 因为笔者用的是 中文window, 因此默认是GBK. 编码没问题, 但读出来的时候缺没根据 Charset.defaultCharset() 来进行编码.
下面的 myBuffer 就是用GBK进行编码转成字节数组的, 但 Gradle Plugin 读的时候却用了UTF-8, 用的是 StringBuilder , toString 只支持Latin1和UTF-8 类型的, 不支持GBK
解决办法
所以 StringBuilder 的 toString 也是个坑, 竟然没有根据 Charset.defaultCharset() 来编码. 也可以说是Gradle Plugin 的坑, 用了不支持GBK的StringBuilder.
所以能改的只能修改 Gradle Plugin 的编码了, 把前面提到的GBK改成UTF-8, 前面提到改 Charset.defaultCharset() 的方式就是 -Dfile.encoding=UTF-8 , 因为Gradle Plugin 和IDEA是同一个进程, 所以需要修改IDEA 的 -Dfile.encoding=UTF-8 .
总结和收获
- 向上面那样细致的定位问题会有点小题大做. 在 java 里面 String 默认都是通过UTF-8编译的, 在控制台看到变量是没有乱码的, 证明编码还是正常的. 因此在debug 的时候通过查看String 变量的值是最简单的方式.
- 系统的编码大部分是根据 Charset.defaultCharset() (默认根据操作系统, 可使用 -Dfile.encoding 来指定) 进行编解码的, 这样的好处是系统内部的编码是统一的, 只要大家都按照 Charset.defaultCharset() 来, 那就不会有问题. 所以我们编码的时候最好不要指定编码方式, 而是通过Charset.defaultCharset()来指定, 这样乱码的风险会小一些.