博客 / 詳情

返回

IDEA 使用 gradle 亂碼之謎

目標

瞭解亂碼的成因
瞭解亂碼的定位方式和解決方法

為什麼需要編碼呢?

因為字符串是需要編碼成字節數組作為載體的來存儲和傳輸.

為什麼會亂碼?

亂碼產生的原因一般是因為編碼轉換出錯. 字符串常見編碼有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()來指定, 這樣亂碼的風險會小一些.
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.