其實要在多套不同的 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 的程序中,因此執行速度非常快!👍
安裝步驟如下:
-
安裝 google-java-format plugin 並 Restart IDE
-
修正 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 · GitHub 與 What's the difference between --add-exports and --add-opens in Java 9? - Stack Overflow。
-
由於 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 格式化專案原始碼
-
先到 google-java-format
的 Releases 頁面下載 google-java-format-eclipse-plugin-1.13.0.jar
-
直接將 google-java-format-eclipse-plugin-1.13.0.jar
檔案儲存到 Eclipse 或 STS4 的 drop-ins
資料夾 (The dropins folder and supported file layouts)
-
點選主選單的 Window
> Preferences
開啟 Preferences 視窗
-
點選左側頁籤到 Java
> Code Style
> Formatter
並選取 Formatter Implementation
下拉選單,此時你會看到 google-java-format
這個選項,選完按下套用即可設定完成!
在 Visual Studio Code 之中,雖然 VSCode Marketplace 有個 google-java-format 擴充套件,但這套並非 Google 官方支援的版本,且他骨子裡實際上就是在你每次執行 Format Document
(格式化文件) 時,執行外部 google-java-format
命令而已,每次執行格式化動作都會啟動一次 google-java-format
程序,因此執行效能有稍微差一點,而且並沒有什麼選項可以設定。
我參考了 Using google-java-format with VS Code 文章的建議,以目前來說,我也認為最建議的設定方式是:
- 在儲存之前,使用 Language Support for Java(TM) by Red Hat 擴充套件內建的格式化設定(請參考我上一篇文章
- 在儲存之後,透過 Run on Save 擴充套件執行
google-java-format
命令對整個檔案再格式化一次,這樣就可以兼顧效率與風格一致性了!👍
其設定步驟如下:
-
安裝 VSCode 的 Run on Save 套件
這個套件可以讓你設定,當 *.java
檔案在儲存之後,要執行什麼命令!
-
將 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
環境變數中,否則上述設定將無法執行。
-
同時設定「存檔前」與「存檔後」的格式化行為
以下是使用者設定或工作區設定完整的設定內容:
{
"[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 ? #663 與 Adding 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 的過程中查出是否有團隊成員沒有套用團隊要求的程式碼編寫風格! 👍
相關連結