å. 前言
現在的大部分 Java 應用基本都是通過 Maven 進行組織的,不論是分散式應用還是單體叢集應用往往都會通過一個 父 POM 加若干子 POM 完成專案的組織。然而這種多應用多模組的拆分就帶來了一個巨大的體力成本 --- 發包
舉個例子,說明下為什麼會出現這種情況:
上面這個圖中有兩個應用 portal 和 dump,其中 portal 的四個包是需要對外引用的也就是說 client 、domain、common、log 這幾個包是兩個應用共享的二方包。而共享不可避免的會帶來競爭!
簡單分析會有如下的問題:
- 多應用釋出:dump 中需要在 domain 中新增一些類和方法勢必會導致 portal 應用跟著釋出一次,將程式碼合併到基線
- 版本錯亂:多分支開發時大家使用的 snapshot 版本號不一致,不是在處理衝突就是在處理衝突的路上
- 上線換包:應用釋出前需要將所有程式碼切成同一個正式版,在將程式碼中所有引用版本的地方一一替換
ß. Maven 依賴機制(Dependency Mechanism)
為了解決上面遇到的種種問題,怎麼做才能讓這種頻繁的 發包,替換版本,解決衝突 的流程更加簡便自動化呢?簡單來講我的思路是 集中式版本控制!是不是聽著很耳熟,和大名鼎鼎的 git 的思路剛好相反,接下來就一起來看如何讓流程優雅起來以及踩到 Maven 的一些大坑後又是如何一步步爬起來的。
在此之前我們先看看 Maven 專案到底是如何對模組和包進行組織的。
首先建立一個 Maven 專案,然後在通過上圖的三步你就能完成一個新模組的建立。
結果你會得到如上圖所示的一個父 POM 和兩個子 POM。
2.1 父子 POM
父 POM 核心內容如下:
分為兩個部分,一個部分是父 POM 的宣告,包含 GAV 座標,打包方式必須為 POM,因為需要使用聚合模型,另外一部分就是父工程管理的子模組 modules 標籤。
子 POM 相對要更簡單:
宣告自己的父模組是誰,以及自己的 GAV 座標,可能細心的你發現了這裡他並沒有寫 GroupId 和 Version 這是因為父工程已經聲明瞭,如果沒有特別的版本號和 groupId 的要求直接繼承父工程的內容。
2.2 依賴傳遞
Maven 支援通過父 POM 中的依賴繼承的方式避免開我們手動指定依賴庫的版本。但是傳遞依賴會導致依賴圖迅速增長的特別大,所以 Maven 對於傳遞依賴有一定的限制:
- 當依賴了多個版本的元件時 Maven 只會選擇其中一個版本作為依賴,而選擇的策略稱為:
nearest definition
最短路徑 - 依賴自動引入: 當 A 依賴了 B 而 C 依賴了 A 那麼 C 元件會自動引入 B 元件
- 依賴排除:這個理解起來就很簡單 ,如果不想引入自動引入的一些依賴可以通過 ,排除依賴的手段將其去掉
2.3 依賴範圍
依賴項的範圍決定了什麼時候這些依賴會被載入進去,在 Jar 瘦身等操作的時候特別有用,同時解決依賴衝突也是一把好手
- compile 這個是預設值,也就是沒有寫作用域的依賴項在編譯和執行階段都會被載入到類路徑
- provided 這個和 compile 非常類似只是他僅在編譯和測試階段被載入,執行時不會。例如我們常常使用的 Servlet API 這個 jar 僅僅是在編譯測試需要,執行時 Tomcat 早已為我們準備好了這個 Jar ,如果加了反而會可能導致類衝突
- runtime 此範圍表示編譯時不需要依賴項,但是執行時需要依賴項,例如資料庫的驅動
- test 這個基本都是一些跑單測會依賴的 Jar
- system 從參與度來說,和 provided 相同,不過被依賴項不會從maven倉庫抓,而是從本地檔案系統拿,一定需要配合systemPath屬性使用。
當前專案為 A,A依賴於B,B依賴於C。知道B在A專案中的scope,那麼怎麼知道C在A中的scope呢?這個就需要根據 nexus 的一張表來確定:
比如 A 依賴 B 的範圍為 provided ,B 依賴 C 的範圍為 runtime 的 最終 A 依賴 C 的範圍為 provided
ç. 大坑
在回到我們一開始提出的問題,如果團隊裡三個人開發同一個應用,大家都需要修改二方包的版本號,分支合併一定會衝突。同時引用這個二方包的應用也一定會衝突,因為大家使用的版本號一般都不同,那麼以誰的為準?誰來解決這個衝突?往往因為版本號的問題導致衝突合併半小時應用都不一定可以構建的起來。
同時在釋出上線的時候要改包為正式包,需要替換很多個地方,大家的版本還需要一致,往往需要解決多個地方的版本衝突。
為了解決這個問題,我採用瞭如下的方案:
- 大家在同一個環境開發的時候版本號永遠都保持統一,比如在預發你的包版本只能是
pre0-snapshot
否則分支提交不上去 - 所有的包版本都收束到主 POM 中,禁止單獨在每個 POM 中單獨宣告要釋出或依賴的二方包
改造前後,主 POM樣子如下:
子 POM 中就不在單獨宣告版本號了 而是直接繼承父 POM 中定義的版本號:
這樣確實很好的解決了上面的兩個問題,但是在某次部署過程中遇到了一個非常詭異的問題。
我們專案結構如下:
ProjA
| -- Apache Commons 3.0
|________
| Proj B's Client
| | -- mq-client
| | -- redis-client
| | -- etc.
|
|________
Server
| -- Server Libraries
| -- etc.
A 工程引用了 B 工程的 client 包,而其 client 包中引入了 mq 和 redis 的客戶端,因此 A 工程在不用引入這兩個包的情況下可以直接使用這兩個包中的類。但是在某次部署的過程中,A 工程怎麼都找不到 mq 和 redis 的類檔案,這就讓人摸不著頭腦了,線上都是可以的,為何預發就有這個問題了???
∂. 溯源
又到了緊張而又刺激的問題排查階段了。從 mvn 倉庫上下載了最新的編譯後的包放到 jad 中發現程式碼都是和我的分支保持一致的,沒有啥問題,而且看到 snapshot 包後面的時間戳也是我釋出包的時間戳。
那也就是發包的過程和結果都沒啥問題,肯定是拉包的時候出問題了唄,看看拉包的過程是否有異常。
mvn clean && mvn install -fn
一套命令跑下來,好像也沒有 error,但是包就是拉不下來。看看日誌裡面有什麼貓膩吧!一頓日誌的搜查發現了一行 waring 日誌:應用引入的依賴包無效,依賴包中傳遞依賴項不可用,可以通過開啟debug獲取更多資訊。
[WARNING] the POM for A is invalid, transitive dependencies (if any) will not be available, enable debug logging for more details...
開啟maven debug功能後,警告後緊跟了一條錯誤資訊,如下。
[WARNING] The POM forxx:jar:1.0-SNAPSHOT is invalid, transitive dependencies (if any) will not be available: 2 problems were encountered while building the effective model for xx:1.0-SNAPSHOT
[ERROR] 'dependencies.dependency.version' for xx:jar is missing.
[ERROR] 'dependencies.dependency.version' for xx:jar is missing.
transitive dependencies
這玩意不就是依賴傳遞麼,我已開始還不知道遇到的這個問題如何用文字向搜尋引擎描述,現在顯然就是傳遞依賴的一些包沒有被引入啊,這不就找到問題所在了, 因為下面有兩個包沒有宣告 jar 的包版本。
但是為何會出現這個問題呢?根據上述報錯的關鍵字我在 stackoverflow 中找到了答案:
One reason for this is when you rely on a project for which the parent pom is outdated. This often happens if you are updating the parent pom without installing/deploying it.
To see if this is the case, just run with
mvn dependency:tree -X
and search for the exact error. It will mention it misses things you know are in the parent pom, not in the artifact you depend on (e.g. a jar version). The fix is pretty simple: install the parent pom usingmvn install -N
and re-try
上面短短几句話即說明了原因也給出瞭解決方案,美利堅的程式設計師果然牛皮!描述的大致意思就是因為這個二方包的父 POM 用的是老版本里面沒有包含一些傳遞依賴的 jar 包的版本導致很多包拉不下來。解決方案也很簡單直接把父 POM 中的依賴版本號加上並重新打包釋出下就好了。
回顧上面說的元件的傳遞依賴,這裡的二方包中依賴的 redis 和 mq 的 client 包沒有拉下來是因為二方包 POM 中的某個 jar 的版本號即沒有在父 POM 中定義也沒有在二方 POM 中定義。二方包在找元件的依賴的時候首先會在本 POM 找,如果沒有找到就會根據
<parent>
<artifactId>module-test</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
宣告的父 POM 的版本號去父 POM 中找,因為父 POM 用的老版本里面根本沒有那個包的版本號所以就報了剛才那個錯誤。
所以如果要釋出新的二方包而且想要使用傳遞依賴的特性的話一定要重新發布父 POM !!!