The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何使用 Google Java Format 做到更決斷的 Java 原始碼編排風格

其實要在多套不同的 IDE 開發工具之間統一編碼風格(Coding Style)真的不太容易,不同 IDE 之間的程式碼格式化能力不同,有的強、有的弱,自動排版完多多少少還是會有些差異,因此很難做到真正的統一。因此 Google 已經漸漸移往更為決斷的 google-java-format 格式化工具,不太傾向依賴不同的 IDE 之間的程式碼格式化能力。本篇文章我打算分享我這兩天的研究成果,看如何在不同 Java 工具之間如何做到更完美的風格整合。

安裝 google-java-format 工具

由於 google-java-format 是一個 Java 開發的應用程式,而且封裝成 .jar 檔,因此你只要到 Releases · google/google-java-format 下載最新版 google-java-format-1.15.0-all-deps.jar 執行檔,就可以直接使用。執行命令與用法如下:

java -jar google-java-format-1.15.0-all-deps.jar <options> [files...]
Usage: google-java-format [options] file(s)

Options:
  -i, -r, -replace, --replace
    Send formatted output back to files, not stdout.
  -
    Format stdin -> stdout
  --assume-filename, -assume-filename
    File name to use for diagnostics when formatting standard input (default is <stdin>).
  --aosp, -aosp, -a
    Use AOSP style instead of Google Style (4-space indentation).
  --fix-imports-only
    Fix import order and remove any unused imports, but do no other formatting.
  --skip-sorting-imports
    Do not fix the import order. Unused imports will still be removed.
  --skip-removing-unused-imports
    Do not remove unused imports. Imports will still be sorted.
  --skip-reflowing-long-strings
    Do not reflow string literals that exceed the column limit.
  --skip-javadoc-formatting
    Do not reformat javadoc.
  --dry-run, -n
    Prints the paths of the files whose contents would change if the formatter were run normally.
  --set-exit-if-changed
    Return exit code 1 if there are any formatting changes.
  --lines, -lines, --line, -line
    Line range(s) to format, like 5:10 (1-based; default is all).
  --offset, -offset
    Character offset to format (0-based; default is all).
  --length, -length
    Character length to format.
  --help, -help, -h
    Print this usage statement.
  --version, -version, -v
    Print the version.
  @<filename>
    Read options and filenames from file.

If -i is given with -, the result is sent to stdout.
The --lines, --offset, and --length flags may be given more than once.
The --offset and --length flags must be given an equal number of times.
If --lines, --offset, or --length are given, only one file (or -) may be given.

google-java-format: Version 1.15.0
https://github.com/google/google-java-format

不過,不同的作業系統下也有不錯的套件管理器可安裝,以下列出幾種不同的安裝方式:

  • 透過 Node.js 安裝 google-java-format 全域套件

    npm install -g google-java-format
    

    此套件只是 google-java-format 命令列工具的 Wrapper (封裝),你必須事先安裝 JRE 才能執行。

    安裝好這個 Node.js 版本的 google-java-format 之後,會有一些額外好用的功能。如果你要對 src/ 目錄下所有 *.java 程式碼進行程式碼排版,可以利用 Node.js 常見的 Glob Pattern 來快速選取檔案。例如:你可以利用 --glob=src/**/*.java 參數,一次選取整個資料夾下所有的 *.java 檔案,批次進行格式化作業,這個用法只有這套 Node.js 版本可以做到,相當方便!👍

    google-java-format -i --glob=src/**/*.java
    
  • 使用 Homebrew 安裝 google-java-format 套件

    brew install google-java-format
    

透過 IntelliJ IDEA 的 google-java-format plugin 格式化專案原始碼

由於 IntelliJ IDEA 是一套基於 Java 寫成的開發工具,他的 google-java-format plugin 實際上是把整套 google-java-format 工具都內嵌在 plugin 之中,整個程序會跑在 IntelliJ IDEA 的程序中,因此執行速度非常快!👍

安裝步驟如下:

  1. 安裝 google-java-format plugin 並 Restart IDE

    image

  2. 修正 IntelliJ IDEA 2022.2.x 版本的相容性問題

    由於目前最新版的 IntelliJ IDEA 2022.2.1 無法與 google-java-format plugin 兼容,執行時會發生以下錯誤:

    java.lang.AbstractMethodError: Receiver class com.codota.intellij.common.core.CodotaMain does not define or inherit an implementation of the resolved method 'abstract void beforeApplicationLoaded(com.intellij.openapi.application.Application, java.nio.file.Path)' of interface com.intellij.ide.ApplicationLoadListener.
      at com.intellij.idea.ApplicationLoader.initConfigurationStore(ApplicationLoader.kt:431)
      at com.intellij.idea.ApplicationLoader$initApplication$block$3.apply(ApplicationLoader.kt:156)
      at com.intellij.idea.ApplicationLoader$initApplication$block$3.apply(ApplicationLoader.kt)
      at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1150)
      at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
      at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1773)
      at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1760)
      at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)
      at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)
      at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)
      at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)
      at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
    

    這個問題應該在不久的將來會被解決,但如果你也有遇到這個問題的話,可以先透過主選單的 Help > Edit Custom VM Options 開啟 idea64.exe.vmoptions 設定檔,並且加入以下內容,這個 google-java-format plugin 就可以正常使用了:

    --add-opens=java.base/java.lang=ALL-UNNAMED
    --add-opens=java.base/java.util=ALL-UNNAMED
    --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
    --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
    --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
    --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
    --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
    

    此問題可參見 java.lang.IllegalAccessError: class com.google.googlejavaformat.java.JavaInput · Issue #787 · google/google-java-format · GitHubWhat's the difference between --add-exports and --add-opens in Java 9? - Stack Overflow

  3. 由於 IntelliJ IDEA 的 google-java-format plugin 並不包含格式化 import 的順序 (詳見 IntelliJ, Android Studio, and other JetBrains IDEs),因此必須靠設定 intellij-java-google-style.xml (GoogleStyle) 的方式來解決此問題,請參考我的上一篇文章說明進行設定即可。

透過 Eclipse 或 STS4 的 google-java-format plugin 格式化專案原始碼

  1. 先到 google-java-formatReleases 頁面下載 google-java-format-eclipse-plugin-1.13.0.jar

  2. 直接將 google-java-format-eclipse-plugin-1.13.0.jar 檔案儲存到 Eclipse 或 STS4 的 drop-ins 資料夾 (The dropins folder and supported file layouts)

  3. 點選主選單的 Window > Preferences 開啟 Preferences 視窗

  4. 點選左側頁籤到 Java > Code Style > Formatter 並選取 Formatter Implementation 下拉選單,此時你會看到 google-java-format 這個選項,選完按下套用即可設定完成!

    image

透過 Visual Studio Code 的 Run on Save 擴充套件 + google-java-format CLI 格式化專案原始碼

在 Visual Studio Code 之中,雖然 VSCode Marketplace 有個 google-java-format 擴充套件,但這套並非 Google 官方支援的版本,且他骨子裡實際上就是在你每次執行 Format Document (格式化文件) 時,執行外部 google-java-format 命令而已,每次執行格式化動作都會啟動一次 google-java-format 程序,因此執行效能有稍微差一點,而且並沒有什麼選項可以設定。

我參考了 Using google-java-format with VS Code 文章的建議,以目前來說,我也認為最建議的設定方式是:

  1. 在儲存之前,使用 Language Support for Java(TM) by Red Hat 擴充套件內建的格式化設定(請參考我上一篇文章
  2. 在儲存之後,透過 Run on Save 擴充套件執行 google-java-format 命令對整個檔案再格式化一次,這樣就可以兼顧效率風格一致性了!👍

其設定步驟如下:

  1. 安裝 VSCode 的 Run on Save 套件

    這個套件可以讓你設定,當 *.java 檔案在儲存之後,要執行什麼命令!

  2. emeraldwalk.runonsave 設定到使用者設定工作區設定中,讓任何 *.java 檔案在儲存之後,自動執行 google-java-format -i ${file} 命令,設定內容如下:

    {
      "emeraldwalk.runonsave": {
        "commands": [
          {
            "match": "\\.java$",
            "cmd": "google-java-format -i ${file}"
          },
        ],
      },
    }
    

    設定好之後,每次當你變更 *.java 原始碼時,在儲存之後就會自動執行 google-java-format -i ${file} 命令,將該檔案進行格式化處理。

    請記得將 google-java-format 註冊到 PATH 環境變數中,否則上述設定將無法執行。

  3. 同時設定「存檔前」與「存檔後」的格式化行為

    以下是使用者設定工作區設定完整的設定內容:

    {
        "[java]": {
            "editor.defaultFormatter": "redhat.java",
            "editor.formatOnSave": true
        },
        "java.format.enabled": true,
        "java.format.onType.enabled": true,
        "java.format.settings.url": "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml",
        "java.format.settings.profile": "GoogleStyle",
    
        "emeraldwalk.runonsave": {
          "commands": [
            {
              "match": "\\.java$",
              "cmd": "google-java-format -i ${file}"
            },
          ],
        }
    }
    

目前 Language Support for Java(TM) by Red Hat 擴充套件有在討論如何將 google-java-format 更好的整合到 VSCode 之中,不過感覺沒有很積極的在實作。詳見:Use google-format-code instead of eclipse ? #663Adding section VScode in the Readme file · Issue #488 · google/google-java-format

整合 CI 檢查新的 PR 是否符合團隊編碼風格規範

Google 官方的 google-java-format 命令列工具,如果在執行的時候有任何檔案發生格式變化,其實就代表著目前的原始碼有包含一些不合格的撰寫風格,或是還沒有跑過一次 google-java-format 命令。

當有團隊成員將沒有依照團隊的規定,未將程式碼格式化就發 PR 嘗試將程式碼合併回主線,這時就可以透過 CI 檢查出來,並且自動拒絕本次 PR 拉取請求! 👍

google-java-format 其實有個 --set-exit-if-changed 參數,它可以讓你在執行格式化作業時,在發現有程式碼出現風格不一致的狀況時,自動回應 non-zero 的退出碼 (exit code),而這個 non-zero 的退出碼,預設會讓 CI 自動失敗。以下是執行的範例:

google-java-format --set-exit-if-changed -n --glob=src/**/*.java

如果你希望僅檢查 Git 版控中已變更的檔案是否有符合 google-java-format 風格,那你可以這樣執行:

git --no-pager diff HEAD~..HEAD --name-only > filelist.txt
google-java-format --set-exit-if-changed -n '@filelist.txt'

若是 Azure Pipelines 的話,假設你想要設定 PR 合併回 develop 分支的檢查,其命令如下:

git --no-pager diff develop..$(Build.SourceVersion) --name-only > filelist.txt
google-java-format --set-exit-if-changed -n '@filelist.txt'

總結

google-java-format 命令列用法說明你應該可以發現,你其實根本就沒有什麼機會可以調整格式化設定,他就只有 2 種格式可以讓你選擇而已。一個是預設的 Google Java Style Guide 格式,另一個則是透過 --aosp 參數選用 Android Open Source Project 提供的 AOSP Java Code Style 排版風格。兩者風格差不多,主要差別在於 Google Java Style Guide 的縮排採用 2 個空白字元,而 AOSP 的縮排採用 4 個空白字元。

在嚴格的規範之下,搭配命令列工具的整合,你將更容易做到在不同的開發工具之間,使用更為一致的 Java 程式碼編排風格,版控可以更容易進行,也更容易在 CI 的過程中查出是否有團隊成員沒有套用團隊要求的程式碼編寫風格! 👍

相關連結