共计 4348 个字符,预计需要花费 11 分钟才能阅读完成。
指标
理解乱码的成因
理解乱码的定位形式和解决办法
为什么须要编码呢?
因为字符串是须要编码成字节数组作为载体的来存储和传输.
为什么会乱码?
乱码产生的起因个别是因为编码转换出错. 字符串常见编码有 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 插件)
-
IDEA console (IDEA 的 run 控制台)
执行逻辑
他们的执行逻辑如下:
- 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 外部的逻辑必定是指定了这种解码形式的. 所以异样信息的解码个别不会有问题.
navite 的编码方式, 让 UTF- 8 编码的字节数组转成可视化的 16 进制的字符串, 再对字符串进行编码保留
debug 发现是间接调用 JDK 外面的 JavacTaskImpl 进行编译, 并通过流的形式输入后果
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()来指定, 这样乱码的危险会小一些.