目标

了解乱码的成因
了解乱码的定位方式和解决方法

为什么需要编码呢?

因为字符串是需要编码成字节数组作为载体的来存储和传输.

为什么会乱码?

乱码产生的原因一般是因为编码转换出错. 字符串常见编码有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部分都会对字符串的呈现产生影响:

  1. .class 文件的编码
  2. getBytes("UTF-8") 进行转码获取对应编码的字节数组
  3. new String(,"GBK") 进行解码用于显示字符串
  4. System.out.println 编码后转成字节流写入控制台
  5. 控制台读取字节流数据进行编码后呈现在控制台上面

    .class 文件的编码

    .class 编码默认是UTF-8的. 但 .java 不一定. jdk编译器会把 .java 转成 .class. 意味着.java的编码和编译器程序的解码必须是一致的, (IDEA修改编译编码的方式在备注1)
    否则会出现下面的情况, 虽然 .java里面显示的是 "你好", 但实际上变量的内容是 "浣犲ソ"
    !

图片

看着是编译器是没问题的, 但实际上运行起来确是乱码, 证明 .class出现问题了

image.png

class 里面默认是通过UTF-8编码

getBytes("UTF-8")

只有当class类编码为UTF-8时, 才能拿到正确的字节数组, 否则编码对不上拿到的字节数组就会有问题.

new String(,"GBK")

  1. 通过UTF-8编码之后, 通过GBK解码即可模拟出乱码的情况.

    System.out.println

  2. , 程序会获取到 控制台的输出流, 并往里面写字节流, 这时候又需要再转一次编码.

    控制台

  3. 控制台应用获取到字节流, 然后通过解码展示在控制台应用上.

编解码总结

上面每个地方都有可能会编码产生影响, 在这个程序里面无法得知1,4,5 的编码到底有没有问题, 所以无法知道控制台输出的结果是什么. 看似转来转去很复杂, 实际上只需要清楚3点即可.

  1. 字符串会被编码成字节来存储和传输, 字节是没有乱码. 你看到的中文或者乱码都是通过解码得来的(包括你在编辑器看到的中文).
  2. 在字符串编码之后的字节, 要采用相同的解码方式才不会乱码.

编码的地方有很多, 例如存储和传输, 例如输出到文件(.class), 输出到控制台, String.getBytes() 等等. 解码的地方则例如编辑器看到的中文, 控制台的中文, new String() 其实都在解码.

  1. 一般会有三个地方会影响中文的正常呈现,

    1. 一个是输入, 例如 .class文件, socket
    2. 一个是处理, 也就是内部的转换, 例如String.getBytes() 或者 使用ByteArrayStream自己转了一下 .
    3. 一个是输出, 也就是前面提到的解码.

IDEA 使用gradle时控制台乱码

最近发现一个IDEA里面使用gradle插件的一个乱码问题. 下面是特定搞出来的异常, 是编译错误的异常.
image.png
查了很多资料, 通过 IDEA64.exe.vmoptions 里面增加 -Dfile.encoding=UTF-8 可以解决问题. 但发现修改编码后控制台的显示也会有变, 所以有没有更好的方式呢, 乱码的原因是什么呢? 我能不能修改gradle的编码方式, 和IDEA保持一致, 就不需要修改 -Dfile.encoding 了.

下面的分析方法可能会有点笨, 但如果都搞懂了对乱码的原因会有更深的理解.

IDEA 涉及到 gradle 的逻辑

组成部分

在 IDEA 里面 gradle 从运行到展示由三部分组成:

  1. gradle
  2. Gradle Plugin (gradle的IDEA插件)
  3. IDEA console (IDEA的run控制台)

    执行逻辑

    他们的执行逻辑如下:

  4. Gradle Plugin 首先会通过Process 执行 java gradle-launcher.jar 启动 gradle的deamon 进程.
  5. gradle进程会启动一个端口用于执行真正的gradle指令和输出指令结果, 因此 Gradle Plugin 会找到 deamon开放的端口进行connect, 并传入gradle指令.
  6. Gradle Deamon 是一个独立的进程, 被启动后会执行gradle指令内容, 例如编译, 执行等, 通过socket 来返回异常信息. socket的输出流经过转码呈现在 IDEA 的ConsoleView 上面.

通讯方式

他们的通讯方式如下
image.png

乱码分析

乱码只会在编解码的地方出现, 因此一开始需要先找到存在编解码的地方, 然后再逐个进行分析.

可能存在编解码的地方

根据上面的流程可以看到, 中文的源头应该是在gradle deamon, 因为是gradle deamon负责执行gradle指令的, 我们可以推测出可能存在编码和解码的方式有哪些

  1. JDK JavaCompile 编译产生的异常信息
  2. Gradle Deamon 接收异常信息
  3. gradle deamon-> Gradle Plugin
  4. Gradle Plugin -> IDEA console

image.png

逐个进行编解码分析

1. 异常源头

我们先看这个异常信息是哪里来的. 通过对gradle的debug, 发现异常信息是gradle直接调用 JavacTaskImpl 触发编译过程, 然后jdk通过流的方式把异常输出出来. , jdk 里面的多语言使用的是 native 的编码方式, jdk内部的逻辑肯定是指定了这种解码方式的. 所以异常信息的解码一般不会有问题.

image.png
navite 的编码方式, 让UTF-8编码的字节数组转成可视化的16进制的字符串, 再对字符串进行编码保存

image.png
debug 发现是直接调用 JDK 里面的 JavacTaskImpl 进行编译, 并通过流的方式输出结果

2. 异常输出

JavaCompile 通过流的方式输出, Gradle Deamon 通过流的方式写入.

image.png
下面的框是 JavaCompile 输出流 , 上面的框是 Gradle Deamon 输入流

JavaCompile 输出流
image.png
JDK 通过字节流的方式返回编译异常信息, 并使用 Charset.defaultCharset() 来作为编码

Gradle Deamon 输入流
image.png
image.pngGradle Deamon 通过 buffer 接受字节流, 然后同样通过 Charset.defaultCharset() 来作为解码

写和读都是使用 Charset.defaultCharset() , 所以不会乱码.

2. socket 通讯

Gradle Deamon 的写入

Gradle Deamon 通过读出 javaCompile 的输出流拿到异常的信息, 这时候要通过 socket 传给 Gradle Plugin了. socket 的序列化方式是通过 kryo 来序列化的, 但在序列化的时候默认使用了 UTF-8 的形式进行编码 (writeUtf8), 而非 Charset.defaultCharset() .
image.png

Gradle Plugin 的写出

写完就是 Gradle Plugin 来读写入的信息了, 这里是对 Gradle Plugin 进行 debug 的截图. 因为也是默认使用UTF-8来解码, 所以也没有问题.

image.png 类名为: com.esotericsoftware.kryo.io.Input

3. 控制台交互

debug了一下Gradle Plugin, 在 ConsoleView 这个类中发现了问题. 读还好好的, 怎么在ConsoleView就乱码了.

image.png
com.intellij.execution.impl.ConsoleViewImpl

顺着调用链找到正常中文和乱码的中间地带, 发现有个OutputStreamWriter
image.png

为什么中间还要再编码解码一次呢?
因为 gradle 是一个脚本, 因此输入输出都是默认使用流的方式. 按照一般的用法, 会通过命令行去触发指令, 再把输出流写入到控制台上的. 但Gradle Plugin 刚好是通过自己 connect 的方式而非再起一个进程被动触发, 因此输入输出都在同一个进程里面, 但还是要通过流的方式去获取输出.

OutputStreamWriter 的编码方式是上文提到的 Charset.defaultCharset() , 因为笔者用的是 中文window, 因此默认是GBK. 编码没问题, 但读出来的时候缺没根据 Charset.defaultCharset() 来进行编码.

下面的 myBuffer 就是用GBK进行编码转成字节数组的, 但 Gradle Plugin 读的时候却用了UTF-8, 用的是 StringBuilder , toString 只支持Latin1和UTF-8 类型的, 不支持GBK
image.png
image.png

解决办法

所以 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 .
image.png

总结和收获

  1. 向上面那样细致的定位问题会有点小题大做. 在 java 里面 String 默认都是通过UTF-8编译的, 在控制台看到变量是没有乱码的, 证明编码还是正常的. 因此在debug 的时候通过查看String 变量的值是最简单的方式.
  2. 系统的编码大部分是根据 Charset.defaultCharset() (默认根据操作系统, 可使用 -Dfile.encoding 来指定) 进行编解码的, 这样的好处是系统内部的编码是统一的, 只要大家都按照 Charset.defaultCharset() 来, 那就不会有问题. 所以我们编码的时候最好不要指定编码方式, 而是通过Charset.defaultCharset()来指定, 这样乱码的风险会小一些.

简简单单
18 声望1 粉丝