拯救Java Code Style強迫癥

文/周宇剛
擁有 10 年的 JAVA EE 開發經驗,在 ThoughtWorks 擔任高級咨詢師。在加入 ThoughtWorks 之前,在一家國內領先的航旅企業擔任架構師,專注于持續交付實踐和大型企業應用架構治理。
這篇文章緣起于上一個持續交付的咨詢項目,當時正在指導客戶團隊的 Java 工程師做 Code Review,發現一個很有意思的現象:有一位工程師對 Code Style 特別在意,所以在 Code Review 的大部分時間中都是該工程師在指出哪里哪里的格式不對,但是團隊并沒有找到改進方法,每次的結論都是“下次我注意一點。”我挺欣賞這位工程師對 Code Style 的認真態度,所以就萌生了“怎么拯救 Code Style 強迫癥”的想法。
要點
- Code Style 不是個人喜好問題,它會影響工作效率,團隊應將其當做工程實踐予以重視。
- Code Style 需要端到端的工具支持,盡早解決問題,避免技術債。
- 以 Checkstyle 作為核心工具支撐 Java 項目的 Code Style 實施方案。
Code Style 是一項工程實踐

我是右側風格的忠實擁躉,如果讓我在工作的項目中看到左側風格的代碼,你猜猜我的反應是什么。

嗯,可能我對代碼風格確實有些強迫癥,但事實上,Code Style 并不僅僅是代碼是否好看那么簡單,如果沒有按照慣例來編寫代碼,甚至會讓閱讀者產生疑惑。
private Listener listener = new Listener ()
// So Listener looks like a class? {}; // Oops, it is an interface
如果代碼可讀性還不足以打動你,那么想象一下這個場景,你的同事說他修復了兩個空指針問題,請你幫忙 Code Review,你查看了這個文件的修訂歷史,乍看之下有許多改動,看來是個大動作。然而事實上,絕大部分改動是代碼格式調整,只有兩處改動與需要 Review 的問題相關。

(看來這位同事的 IDE 使用了不同的自動縮進設置,導致所有行都產生了縮進)
之所以會產生以上這些影響工作效率的問題,是因為團隊沒有重視 Code Style,沒有把它當做一項工程實踐,既沒有對其達成一致,也沒有正確地使用工具幫助實施。
那就按照工程實踐的標準來實施 Code Style
本文將重點介紹 Java 項目中 Code Style 的工具支持,但在此之前,你的團隊需要一起做一些決定:
- 使用哪種 Code Style?
每個人可能都有偏好的 style,但在團隊協作面前,需要一定的妥協。有些公司或組織有著統一的 Code Style 指導標準,蕭規曹隨是個不錯的選擇(但是要確保這類統一指導標準在制定時參考了開發人員的意見,是切實可行的),你的團隊也可以自己裁剪,但至少要保證項目(Repository)級別上使用同一種 Style。
- 如何處理不符合 Code Style 的提交?
大家往往懈怠于事后補救的方式,我的建議是不要讓不符合約定的代碼流入代碼庫。對于遺留項目,尤其是大型項目,可以選擇一部分代碼作為實施范圍,集中修復 Style 問題后嚴格實施,切忌操之過急,最后團隊疲憊不堪只得放棄。
我們都知道人工監督檢查的方式是不可持續和不可靠的,來看看有哪些工具可以提供幫助吧。
懶惰是第一生產力
工程實踐不能沒有自動化工具支持,在 Java 生態圈中,Code Style 工具最出名的應該是 Checkstyle 了,它可以通過 XML 形式的外部 DSL 來定義 Code Style 的檢查風格,比如你可以從這里找到 Google 的 Java Checkstyle 配置文件。這里我不會詳細介紹 Checkstyle 本身,相反,我會更多地探討如何工程化地使用 Checkstyle,在交付代碼的各個活動中,我們都可以用到 Checkstyle,進行 360°無死角的檢查。

(和 Code Style 相關的代碼交付生命周期)
守住提交的質量關口
為了貫徹不讓不符合約定的代碼流入代碼庫的決定,可以優先在服務端設置 Code Style 的檢查關卡。

(優先守住代碼提交時的服務端檢查,可以考慮使用 CI 服務器來實現)
從實現層面上說,有兩種方式:
一是在 SCM(Source Control Management,例如 Git/SVN)服務端設置檢查項,如果不達標則拒絕提交,但這種方式相對不容易實現,而且一般 SCM 服務端也不由開發團隊管理,設置起來不靈活也不方便。
二是利用持續集成服務器,開發團隊的每一次提交都會觸發一次構建,我們可以在構建腳本中加入 Checkstyle 檢查,如果有不達標的代碼則讓構建失敗,以便告訴提交者立即修復 Style 問題。我更推薦這個方案,因為相關的工具支持都很成熟,實現簡單,而且構建過程可以在開發者的本地環境復制,以便在后續改進中將 Checkstyle 檢查前移,提供更快的反饋。如果團隊使用 Maven/Gradle 等構建工具,可以用插件的方式實現 Checkstyle 檢查并嵌入到整個構建過程中。這樣 CI 服務器只要調用構建腳本就行了。
在開發者本地驗證 Style

(在開發者本地實現驗證,反饋關口前移)
在實現了 CI 驗證后,就可以著手實現開發者本地驗證了,這樣開發者就不用等到提交代碼到服務端后才會獲得反饋了。由于之前采用的是構建工具的插件方案,所以開發者在本地運行構建就能實現驗證了。比如 Gradle 提供了 Checkstyle 插件支持,你可以在這里找到 Gradle Checkstyle Plugin 的詳細配置文檔,如果你使用 Maven,則可以參考這里。現在只需要一條命令,開發者久能在本地驗證 Code style 了。
# build.gradle # omitted plugins apply plugin: 'checkstyle' checkstyle { configFile = file ("config/checkstyle.xml") //指定 checkstyle 配置文件
toolVersion = "7.4" //指定 checkstyle 工具的版本,部分 style 規則有版本要求
} checkstyleTest.exclude "**/ContractVerifierTest**" // 忽略檢查生成代碼,這個鍋我們不背 // 如果出現 checkstyle warning 也使構建失敗,插件默認只支持 checkstyle error 失敗 // Fail build on Checkstyle Warning Violation · Issue #881
tasks.withType (Checkstyle) .each { checkstyleTask -> checkstyleTask.doLast { reports.all { report -> def outputFile = report.destination if (outputFile.exists () && outputFile.text.contains ("<error ")) { throw new GradleException ("There were checkstyle warnings! For more info check $outputFile") } } } }
現在只需要一條命令,每個開發者就能在本地驗證 Code Style 了。你可以在這里找到 Gradle Checkstyle Plugin 的詳細配置文檔,如果你使用 Maven,則可以參考這里。
? court-booking-backend (master) ? ./gradlew check Starting a Gradle Daemon (subsequent builds will be faster) :compileJava :processResources UP-TO-DATE :classes :checkstyleMain [ant:checkstyle] [WARN] /Users/twer/Workspace/restbucks/court-booking-backend/src/main/java/com/restbucks/courtbooking/http/CourtRestController.java:16: 'method def' child have incorrect indentation level 4, expected level should be 8. [Indentation] :checkstyleMain FAILED FAILURE: Build failed with an exception.
本地驗證很不錯,但我有時候會忘記執行

(讓機器代勞瑣事)
有時候,開發者修改了代碼后會忘記執行本地檢查就提交代碼了,最好能夠在提交代碼前自動執行檢查。如果你使用 Git 的話,可能會想到 Git commit hook,比如這是我常用的 pre-commit hook
#!/bin/sh # From gist at https://gist.github.com/chadmaughan/5889802
# stash any unstaged changes git stash -q --keep-index # run the tests with the gradle wrapper ./gradlew clean build # store the last exit code in a variable RESULT=$? # unstash the unstashed changes git stash pop -q # return the './gradlew build' exit code exit $RESULT
將該腳本拷貝到.git/hooks/下,在執行git commit的時候就會自動觸發檢查了,如果檢查失敗則提交失敗。但問題是.git并不能提交到遠程代碼倉庫,那么除了人工分發和拷貝外,有沒有更好的方式在團隊中共享這個機制呢?
可以曲線救國!把 pre-commit 納入版本控制(如下面的config/pre-commit),再使用構建工具的擴展機制來自動完成拷貝工作,這樣可以間接實現 git hooks 的團隊間共享。
# build.gradle task installGitHooks (type: Copy) { //將 pre-commit 拷貝到指定位置
from new File (rootProject.rootDir, 'config/pre-commit') into { new File (rootProject.rootDir, '.git/hooks') } fileMode 0755 } build.dependsOn installGitHooks //設置執行 build 任務時會自動觸發 installGitHooks 任務
關閉包圍圈,編輯時反饋

(實時反饋)
之前基于構建工具的方案都很好,但是對于開發者來說,最好能將反饋前移到編輯時,并且可視化。所幸的是,Checkstyle 的生態系統非常成熟,各主流 IDE 都有插件支持,以 Intellij Idea 為例,可以使用 checkstyle-idea 插件,讓團隊成員手工設置插件,使用項目的 checkstyle 配置文件即可(我目前還沒有找到自動化配置的方式,或許 gradle idea 插件可以?)


(checkstyle-idea 插件配置和效果)
有了自動實時檢查,最好還能將 IDE 的自動格式化與 Checkstyle 配置文件掛鉤,否則自動格式化反倒給你添麻煩了。

(為 IDE 導入 checkstyle 配置文件作為自動格式化的依據)
如果你連自動格式化都懶得按,那可以試試 Save Actions 插件,它可以在 Intellij 保存文件時自動執行代碼格式化等動作。

(這個插件目前對部分文件有些問題,可以通過 File path exclusion 忽略)
總結
- Code Style 影響工作效率,團隊應將其當做工程實踐予以重視。
- Code Style 不能靠人工監督和檢查,應該提供端到端的工具支持
- 服務端檢查(推薦集成到 CI 的構建步驟中)
- 開發環境檢查(使用各構建工具的 Checkstyle 插件)
- 自動提交檢查(git pre-commit hook 與共享)
- IDE 增強(checkstyle 插件實時可視化反饋/自動的自動格式化!)
- 以上的工具都要依據為同一份 Checkstyle 配置文件,并納入版本控制
希望以上這些招數可以解救 Java Code Style 強迫癥 :)
來自: insights.thoughtworkers.org