高效使用Java構建工具|Maven篇|雲效工程師指北

語言: CN / TW / HK

大家好,我是胡曉宇,目前在雲效主要負責Flow流水線編排、任務調度與執行引擎相關的工作。

作為一個有多年Java開發測試工具鏈開發經驗的CRUD專家,使用過所有主流的Java構建工具,對於如何高效使用Java構建工具沉澱了一套方法。眾所周知,當前最主流的Java構建工具為Maven/Gradle/Bazel,針對每一個工具,我將分別從日常工作中常見的場景問題切入,例如依賴管理、構建加速、靈活開發、高效遷移等,針對性地介紹如何高效靈活地用好這3個工具。

Java構建工具的前世今生

在上古時代,Java的構建都在使用make,編寫makefile來進行Java構建有非常多彆扭與不便的地方。

緊接着Apache Ant誕生了,Ant可以靈活的定義清理編譯測試打包等過程,但是由於沒有依賴管理的功能,以及需要編寫複雜的xml,還是存在着諸多的不便。

隨後Apache Maven誕生了,Maven是一個依賴項管理和構建自動化工具,遵循着約定大於配置的規則。雖然也需要編寫xml,但是對於複雜工程更加容易管理,有着標準化的工程結構,清晰的依賴管理。此外,由於Maven本質上是一個插件執行框架,也提供了一定的開放性的能力,我們可以通過Maven的插件開發,為構建構成創造一定的靈活性。

但是由於採用約定大於配置的方式,喪失了一定的靈活性,同時由於採用xml管理構建過程與依賴,隨着工程的膨脹,配置管理還是會帶來不小的複雜度,在這個背景下,集合了Ant與Maven各自優勢的Gradle誕生了。

Gradle也是一個集合了依賴管理與構建自動化的工具。首要的他不再使用XML而是基於Groovy的DSL來描述任務串聯起整個構建過程,同時也支持插件提供類似於Maven基於約定的構建。除了在構建依賴管理上的諸多優勢之外,Gradle在構建速度上也更具優勢,提供了強大的緩存與增量構建的能力。

除了以上Java構建工具之外,Google在2015年開源了一款強大,但上手難度較大的分佈式構建工具Bazel,具有多語言、跨平台、可靠增量構建的特點,在構建上可以成倍提高構建速度,因為它只重新編譯需要重新編譯的文件。Bazel也提供了分佈式遠程構建和遠程構建緩存兩種方式來幫助提升構建速度。

目前業內使用Ant的人已經比較少,主要都在用Maven、Gradle和Bazel,如何真正基於這三款工具的特點發揮出他們最大的效用,是這個系列文章要幫大家解決的問題。先從Maven説起。

優雅高效地用好Maven

當我們正在維護一個Maven工程時,關注以下三個問題,可以幫助我們更好的使用Maven。

● 如何優雅的管理依賴

● 如何加速我們的構建測試過程

● 如何擴展我們自己的插件

優雅的依賴管理

在依賴管理中,有以下幾個實踐原則,可以幫助我們優雅高效的實現不同場景下的依賴管理。

● 在父模塊中使用dependencyManagement,配置依賴

● 在子模塊中使用dependencies,使用依賴

● 使用profiles,進行多環境管理

以我在日常開發中維護的一個標準的spring-boot多模塊Maven工程為例。

工程內各個module之間的依賴關係如下,通常這也是標準的 spring-boot restful api多模塊工程的結構。

便捷的依賴升級

通常我們在依賴升級的時候會遇到以下問題:

● 多個依賴關聯升級

● 多個模塊需要一起升級

在父模塊的pom.xml中,我們配置了基礎的spring-boot依賴,也配置了日誌輸出需要的logback依賴,可以看出,我們遵循了以下的原則:

(1)在所有子模塊的父模塊中的pom中配置dependencyManagement,統一管理依賴版本。在子模塊中直接配置依賴,不用再糾纏於具體的版本,避免潛在的依賴版本衝突。

(2)把groupId相同的依賴,配置在一起,比如groupId為org.springframework.boot,我們配置在了一起。

(3)把groupId相同,但是需要一組依賴共同提供功能的artifactId,配置在一起,同時將版本號抽取成變量,便於後續一組功能共同的版本升級。比如spring-boot依賴的版本抽取成了spring-boot.version。

在子模塊build-engine-api的pom.xml中,由於在父pom中配置了 dependencyManagement中依賴的spring-boot相關依賴的版本,因此在子模塊的pom中,只需要在dependencies中直接聲明依賴,確保了依賴版本的一致性。

合理的依賴範圍

Maven依賴有依賴範圍(scope)的定義,compile/provieded/runtime/test/system/import,原則上,只按照實際情況配置依賴的範圍,在必要的階段,只引入必要的依賴。

90%的Java程序員應該都使用過org.projectlombok:lombok來簡化我們的代碼,其原理就是在編譯過程中將註解轉化為Java實現。因此該依賴的scope為provided,也就是編譯時需要,但在構建出最終產物時又需要被排除。

當你的代碼需要使用jdbc連接一個mysql數據庫,通常我們會希望針對標準 JDBC 抽象進行編碼,而不是直接錯誤的使用 MySQL driver實現。這個時候依賴的scope就需要設置為runtime。這意味着我們在編譯時無法使用該依賴,該依賴會被包含在最終的產物中,在程序最終執行時可以在classpath下找到它。

在子模塊dao中,我們有對sql進行測試的場景,需要引入內存數據庫h2。

因此,我們將h2的scope設置為test,這樣我們在測試編譯和執行時可以使用,同時避免其出現在最終的產物中。

更多關於scope的使用,可以參考官方幫助文檔。

多環境支持

舉個簡單的例子,當我們的服務在公有云部署時,我們使用了一個雲上版本為8.0的MySQL,而當我們要進行專有云部署時,用户提供一個自運維的版本為5.7的MySQL。因此,我們在不同的環境中使用不同的 mysql:mysql-connector-java 版本。

類似的,在項目實際的開發過程中,我們經常會面臨同一套代碼。在多套環境中部署,存在部分依賴不一致的情況。

關於profiles的更多用法,可以參考官方幫助文檔

依賴糾錯

如果你已經在父pom中使用dependencyManagement來鎖定依賴版本,大概率的,你幾乎很少會碰到依賴衝突的情況。

但是當你還是意外的看到了NoSuchMethodError,ClassNotFoundException 這兩個異常的時候,有以下兩個方法可以快速的幫你糾錯。

(1)通過依賴分析找到衝突的依賴

(2)通過添加stdout代碼找到衝突的類實際是從哪個依賴中查找的

通過具體的路徑中對應的版本信息,找到對應的版本並校正。

當然這個方法也可以糾出一些依賴被錯誤的加載到classpath下,非工程本身依賴配置引起的衝突。

測試構建過程加速

作為一個開發者,總會希望我們的工程無論在什麼情況下,執行的又快又穩,那麼在Maven的使用過程中,需要遵循以下原則。

● 儘可能複用緩存

● 儘可能的並行構建或測試

依賴下載加速

通常情況下,根據Maven配置文件 ${user.home}/.m2/settings.xml 中的配置,默認情況下是緩存在${user.home}/.m2/repository/。

通常在構建過程中,依賴的下載往往會成為比較耗時的部分,但是通過一些簡單的設置,我們可以有效的減少依賴的下載與更新。

● 優化updatePolicy設置

updatePolicy指定了嘗試更新的頻率。Maven 會將本地 POM 的時間戳(存儲在存儲庫的 maven-metadata 文件中)與遠程進行比較。選項包括:always(總是)、daily(每天,默認值)、interval:X(其中 X 是以分鐘為單位的整數)、never(從不)。

● 使用離線構建

除此之外,如果構建環境已經存在緩存,可以使用Maven的offline模式進行構建,避免依賴或插件的下載更新。

直觀的,日誌中將不會出現類似如下Downloading相關的信息。

構建過程加速

在默認情況下,Maven構建的過程並不會充分的使用你的硬件的全部能力,他會順序的構建你的maven工程的每一個模塊。這個時候,如果可以使用並行構建,那麼將有機會提升構建速度。

以上是並行構建的兩個命令,可以根據實際的cpu情況來選擇對應的命令。但是如果你發現構建時間並沒有得到減少,那麼你的maven模塊間可能存在類似的依賴,模塊之間只是一個簡單的傳遞。

那麼並行構建對你來説並不適用,如果你的模塊間依賴關係存在並行的可能,那麼使用上述命令進行構建,才能使並行構建發揮效果。

測試過程加速

當我們嘗試加速maven工程測試用例的部分,那麼就不得不提到一個插件,maven-surefire-plugin。

當你在執行mvn test的時候,默認情況下就是surefire插件在工作。如果我們想在測試中使用並行的能力,可以作如下配置。

<p style="text-align:center"> </p>

但是需要注意不恰當的使用並行能力進行測試,反而可能帶來副作用。比如當parallel配置為methods,但是由於某些原因測試用例的執行之間存在順序要求,反而會出現因為用例方法並行執行,導致用例失敗,因此也倒逼我們,如果想獲得更快的測試速度,case的編寫也需要獨立且高效。

更多關於surefire插件的使用,可以參考這篇文檔。

Maven插件開發

maven本質上是一個插件執行框架,所有的執行過程,都是由一個一個插件獨立完成的。關於maven的核心插件可以參考這篇文檔。

maven默認為我們提供的這些插件比如maven-install-plugin/mvn-surefire-plugin/mvn-deploy-plugin外,還有一些三方提供的插件,單測覆蓋率插件mvn-jacoco-plugin,生成api文檔的swagger-maven-plugin等等。

在日常工作的過程中,我碰到了這樣一個問題:有個存在明顯問題的sql被髮布到了預發佈環境,同時由於預發與生產使用的是同一個db實例,由於sql的性能問題,影響了線上。

除了通過必要的code review准入,來避免類似的問題,更簡單的,我們可以自己動手實現一個代碼中sql掃描的插件,讓代碼在CI時直接失敗掉,自動化的避免此類問題的發生。於是我們開發了一個maven插件,使用方法和效果如下:

在工程中引入我們開發並部署好的插件com.aliyun.yunxiao:mybatis-sql-scan。

執行以下命令,或其他包含validate階段執行的命令。

我們將會在日誌中看到如下插件執行的信息

在掃描出缺陷時,build失敗,並會在日誌中出現對應的信息:

在GlobalLockMapper.java這個文件中,我們有一條全表掃描的sql語句可能存在風險,

同時build失敗。

接下來我會從如何開發這個異常sql掃描的maven插件入手,幫助大家瞭解插件開發的過程。

1、創建工程

生成的sample工程如下,

其中MyMojo.java定義了插件的入口實現,

此外在根pom.xml中可以看到,

● packaging為“maven-plugin”。

● 依賴配置中,依賴了一些插件開發的基礎二方庫。

● 插件節點下,依賴了maven-plugin-plugin協助我們完成插件的構建。

2、Mojo實現

在開始實現我們的Mojo之前,我們需要做如下分析:

● 插件在maven的哪個生命週期執行

● 插件在執行時需要哪些入口參數

● 插件執行完成後怎麼退出

由於我們要實現的插件是要做mybatis annotation掃描比如 @Update/@Select,判斷是否有異常的sql,比如是否存在全表掃描的sql,是否存在全表更新的sql等,對於此種場景下,

● 由於需要掃描特定的源碼,需要知道工程源碼的所在目錄,以及掃描哪些文件

● 插件掃描出異常時,只要報錯即可,不用產出任何報吿

● 希望在後續執行mvn validate時觸發掃描

那麼預期中的插件是這樣的,

那麼,

● @Mojo(name = "check") 定義了goal

● @Parameter

○ @Parameter(defaultValue = "${project}", readonly = true) 參數綁定了工程的根目錄 ,project.getCompileSourceRoots()便可以獲取到源代碼的根路徑

○ 我們定義了mapperFiles,用來負責掃描哪些文件的通配,excludeFiles用來負責排除哪些文件

● execute()

○ 有了以上的基礎,在execute方法中我們便可以實現對應的邏輯,當掃描結出異常的sql時,拋出MojoFailureException異常,插件便會失敗終止。

以上,我們便完成了一個插件的基本能力的開發。

3、插件的打包與上傳

插件開發完成後,我們可以通過配置distributionManagement,然後執行mvn deploy,完成插件的構建與發佈。

希望通過我的介紹,能夠幫助大家更好的使用maven,下一篇我們講Gradle,歡迎持續關注我們。

點擊下方鏈接,即可免費體驗雲效流水線Flow。

https://www.aliyun.com/product/yunxiao/flow?channel=yy_practice