目標
瞭解亂碼的成因
瞭解亂碼的定位方式和解決方法
為什麼需要編碼呢?
因為字符串是需要編碼成字節數組作為載體的來存儲和傳輸.
為什麼會亂碼?
亂碼產生的原因一般是因為編碼轉換出錯. 字符串常見編碼有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()來指定, 這樣亂碼的風險會小一些.