簡介:高效使用Java建構工具|Maven篇。衆所周知,目前最主流的Java建構工具為Maven/Gradle/Bazel,針對每一個工具,我将分别從日常工作中常見的場景問題切入,例如依賴管理、建構加速、靈活開發、高效遷移等,針對性地介紹如何高效靈活地用好這3個工具。
大家好,我是胡曉宇,目前在雲效主要負責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插件在工作。如果我們想在測試中使用并行的能力,可以作如下配置。
但是需要注意不恰當的使用并行能力進行測試,反而可能帶來副作用。比如當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,歡迎持續關注我們。
原文連結:301 Moved Permanently
本文為阿裡雲原創内容,未經允許不得轉載。