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

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

### 安裝 google-java-format 工具

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

```bash
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](https://nodejs.org/) 安裝 [google-java-format](https://www.npmjs.com/package/google-java-format) 全域套件
    
    ```bash
    npm install -g google-java-format
    ```
    
    > 此套件只是 [google-java-format](https://github.com/google/google-java-format) 命令列工具的 Wrapper (封裝)，你必須事先安裝 JRE 才能執行。
    
    安裝好這個 Node.js 版本的 `google-java-format` 之後，會有一些額外好用的功能。如果你要對 `src/` 目錄下所有 `*.java` 程式碼進行程式碼排版，可以利用 Node.js 常見的 [Glob Pattern](https://www.npmjs.com/package/glob) 來快速選取檔案。例如：你可以利用 `--glob=src/**/*.java` 參數，一次選取整個資料夾下所有的 `*.java` 檔案，批次進行格式化作業，這個用法只有這套 Node.js 版本可以做到，相當方便！👍
    
    ```bash
    google-java-format -i --glob=src/**/*.java
    ```
    
-   使用 [Homebrew](https://brew.sh/zh-tw/) 安裝 [google-java-format](https://formulae.brew.sh/formula/google-java-format) 套件
    
    ```bash
    brew install google-java-format
    ```
    

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

由於 IntelliJ IDEA 是一套基於 Java 寫成的開發工具，他的 [google-java-format plugin](https://plugins.jetbrains.com/plugin/8527-google-java-format) 實際上是把整套 `google-java-format` 工具都內嵌在 plugin 之中，整個程序會跑在 IntelliJ IDEA 的程序中，因此執行速度非常快！👍

安裝步驟如下：

1.  安裝 [google-java-format](https://plugins.jetbrains.com/plugin/8527-google-java-format) plugin 並 Restart IDE
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/df1c98c2b89045d6ddf6-188316473-f2c9e2c2-773b-49b7-86b7-b3189eb4f6d7.png)
    
2.  修正 IntelliJ IDEA 2022.2.x 版本的相容性問題
    
    由於目前最新版的 IntelliJ IDEA `2022.2.1` 無法與 [google-java-format](https://plugins.jetbrains.com/plugin/8527-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](https://plugins.jetbrains.com/plugin/8527-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 · GitHub](https://github.com/google/google-java-format/issues/787#issuecomment-1158034061) 與 [What's the difference between --add-exports and --add-opens in Java 9? - Stack Overflow](https://stackoverflow.com/a/44059335/910074)。
    
3.  由於 IntelliJ IDEA 的 [google-java-format plugin](https://plugins.jetbrains.com/plugin/8527-google-java-format) 並不包含格式化 `import` 的順序 (詳見 [IntelliJ, Android Studio, and other JetBrains IDEs](https://github.com/google/google-java-format#intellij-android-studio-and-other-jetbrains-ides))，因此必須靠設定 `intellij-java-google-style.xml` (`GoogleStyle`) 的方式來解決此問題，請參考我的[上一篇文章](/post/2022/09/03/Google-Java-Style-Guide-for-Eclipse-STS4-and-IntellJ-IDEA)說明進行設定即可。
    

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

1.  先到 `google-java-format` 的 [Releases](https://github.com/google/google-java-format/releases) 頁面下載 `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](https://help.eclipse.org/latest/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Freference%2Fmisc%2Fp2_dropins_format.html))
    
3.  點選主選單的 `Window` > `Preferences` 開啟 **Preferences** 視窗
    
4.  點選左側頁籤到 `Java` > `Code Style` > `Formatter` 並選取 `Formatter Implementation` 下拉選單，此時你會看到 `google-java-format` 這個選項，選完按下套用即可設定完成！
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/39f6157d031aa7beb775-188392246-fd4b0cba-fecf-4628-bea5-3f5960428539.png)
    

### 透過 Visual Studio Code 的 [Run on Save](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) 擴充套件 + [google-java-format](https://github.com/google/google-java-format) CLI 格式化專案原始碼

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

我參考了 [Using google-java-format with VS Code](https://www.sethvargo.com/using-google-java-format-with-vs-code/) 文章的建議，以目前來說，我也認為最建議的設定方式是：

1.  在儲存之前，使用 [Language Support for Java(TM) by Red Hat](https://marketplace.visualstudio.com/items?itemName=redhat.java) 擴充套件內建的格式化設定（請參考我[上一篇](/post/2022/09/03/Google-Java-Style-Guide-for-Eclipse-STS4-and-IntellJ-IDEA)文章
2.  在儲存之後，透過 [Run on Save](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) 擴充套件執行 `google-java-format` 命令對整個檔案再格式化一次，這樣就可以兼顧**效率**與**風格一致性**了！👍

其設定步驟如下：

1.  安裝 VSCode 的 [Run on Save](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) 套件
    
    這個套件可以讓你設定，當 `*.java` 檔案在儲存之後，要執行什麼命令！
    
2.  將 `emeraldwalk.runonsave` 設定到**使用者設定**或**工作區設定**中，讓任何 `*.java` 檔案在儲存之後，自動執行 `google-java-format -i ${file}` 命令，設定內容如下：
    
    ```json
    {
      "emeraldwalk.runonsave": {
        "commands": [
          {
            "match": "\\.java$",
            "cmd": "google-java-format -i ${file}"
          },
        ],
      },
    }
    ```
    
    設定好之後，每次當你變更 `*.java` 原始碼時，在儲存之後就會自動執行 `google-java-format -i ${file}` 命令，將該檔案進行格式化處理。
    
    > 請記得將 `google-java-format` 註冊到 `PATH` 環境變數中，否則上述設定將無法執行。
    
3.  同時設定「存檔前」與「存檔後」的格式化行為
    
    以下是**使用者設定**或**工作區設定**完整的設定內容：
    
    ```json
    {
        "[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](https://marketplace.visualstudio.com/items?itemName=redhat.java) 擴充套件有在討論如何將 [google-java-format](https://github.com/google/google-java-format) 更好的整合到 VSCode 之中，不過感覺沒有很積極的在實作。詳見：[Use google-format-code instead of eclipse ? #663](https://github.com/redhat-developer/vscode-java/issues/663) 與 [Adding section VScode in the Readme file · Issue #488 · google/google-java-format](https://github.com/google/google-java-format/issues/488)。

### 整合 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 自動失敗。以下是執行的範例：

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

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

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

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

```bash
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](https://google.github.io/styleguide/javaguide.html) 格式，另一個則是透過 `--aosp` 參數選用 [Android Open Source Project](https://source.android.com/) 提供的 [AOSP Java Code Style](https://source.android.com/docs/setup/contribute/code-style) 排版風格。兩者風格差不多，主要差別在於 Google Java Style Guide 的縮排採用 `2` 個空白字元，而 AOSP 的縮排採用 `4` 個空白字元。

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

### 相關連結

-   [google-java-format](https://github.com/google/google-java-format)
    -   [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
    -   [AOSP Java Code Style](https://source.android.com/docs/setup/contribute/code-style)
-   IntelliJ IDEA
    -   [google-java-format plugin](https://plugins.jetbrains.com/plugin/8527-google-java-format)
    -   [Advanced configuration | IntelliJ IDEA - JVM options](https://www.jetbrains.com/help/idea/tuning-the-ide.html#configure-jvm-options)
    -   [java.lang.IllegalAccessError: class com.google.googlejavaformat.java.JavaInput · Issue #787 · google/google-java-format](https://github.com/google/google-java-format/issues/787#issuecomment-1158034061)
-   Visual Studio Code
    -   [Run on Save](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave)
    -   [Using google-java-format with VS Code | Seth Vargo](https://www.sethvargo.com/using-google-java-format-with-vs-code/)
-   Git
    -   [How to list only the names of files that changed between two commits - Stack Overflow](https://stackoverflow.com/a/68248959/910074)
