本文將記錄Maven工程中依賴解析機制,內容包括:
- Maven依賴基本結構
- 從倉庫解析依賴的機制
- 依賴傳遞性解析實例
1. Maven依賴基本結構
上篇文章記錄了Maven依賴的聚合與繼承,POM中依賴的聲明通過dependency進行定義,並且通過groupId、artifactId及version三項定位Maven庫中的唯一依賴。除了這三項外,還有其他屬性進行限制,如下:
1 <dependencies> 2 <dependency> 3 <groupId>...</groupId> 4 <artifactId>...</artifactId> 5 <version>...</version> 6 <type>...</type> 7 <scope>...</scope> 8 <optional>...</optional> 9 <exclusions> 10 <exclusion> 11 <groupId>...</groupId> 12 <artifactId>...</artifactId> 13 </exclusion> 14 </exclusions> 15 </dependency> 16 </dependencies>
- groupId、artifactId及version三項不再敘說;
- type:依賴類型,對應於項目坐標定義的packaging,默認為jar;
- scope:依賴范圍,包括compile、test、runtime、import、provided、system;
- optional:標記依賴為可選,即依賴沒有傳遞性;
- exclusions:排除傳遞性依賴
1.1 依賴范圍
我們知道,Maven工程約定具有固定的目錄結構,以便於Maven各插件對工程處理,如編譯(compile)插件,會將src/main/java中的類編譯到target/classes目錄下,則編譯對應的classpath即為target/classes。依賴范圍就是控制依賴於編譯classpath(target/classes)、測試classpath(target/test-classes)和運行classpath(以Web工程為例,WEB-INF/classes)的關系。具體依賴范圍的講述可參考官網文檔,在此僅進行稍微總結:
- compile:編譯依賴范圍,對編譯、測試、運行classpath都有效,為默認范圍;
- test:測試依賴范圍,僅對測試classpath有效;
- runtime:運行時依賴范圍,對測試和運行classpath有效,對編譯無效,如JDBC的依賴引入,因為JDK中值聲明了JDBC接口,具體實現由各廠家決定;
- import:導入依賴范圍,對三種classpath不產生實際影響,一般是導入pom類型的依賴,聚合情況下需要聲明打包類型為pom,其中可包含dependencyManagement(如上一篇文章),此時對引入該依賴的工程不產生影響;
- provided:已提供依賴范圍,對測試和編譯classpath有效,對運行時無效,如servlet-api在測試或編譯的時候需要,在運行的時候由容器提供,故不需要重復引入;
- system:系統依賴范圍,與provided范圍一致,需要明確指定該jar包,其不在Maven倉庫中,感覺不太常用。
1.2 依賴傳遞性
依賴傳遞性,舉例說明,比如A引用B,B引用C,正常情況下,A也會引用C依賴,即A經過傳遞間接引用了C(依賴為可選時,需另行處理)。具體不同范圍的依賴經過傳遞后,其依賴范圍的變化如下邊(從官網扣下來的)
依賴傳遞的准則:
- 路徑最近這優先,即在引用傳遞鏈上,獲取出離本POM最近的傳遞性依賴;
- 如果路徑距離相同,則以取POM中的聲明順序靠前的。
1.3 可選依賴
可選依賴不被傳遞。假設項目A依賴於B,B依賴於X和Y,並且X和Y聲明為可選,則X和Y對A就不具有傳遞性了。如果A需要依賴於X或Y,則需要直接引用。
1.4 依賴排除
假設項目A依賴於B,B依賴於C(版本為1.0),此時A會傳遞性依賴於C(1.0)。如果A需要引用C(2.0版本),存在兩種情況:
(1)A直接引用C(2.0),則可以不對B依賴添加exclusions元素;
(2)A在引用B的同時,還引用D(D引用C(2.0)),則可以在引用B的時候使用exclusions元素將C(1.0)排除,也可以將D依賴聲明在B之前。
2. 從倉庫解析依賴的機制
依賴解析的基本過程:當本地倉庫中沒有依賴構件,則Maven從遠程倉庫中下載;當依賴版本為快照版本時,Maven會自動計算最新的快照,並引用。
背后的依賴解析機制概括如下:
(1)當依賴的范圍為system,則從本機文件系統中解析構件;
(2)根據依賴坐標計算定位依賴位置后,嘗試從本地倉庫尋找依賴,若找到,則解析成功;
(3)若本地倉庫沒有對應構件,則遍歷所有遠程倉庫,發現后解析下載;
(4)如果依賴的版本為RELEASE或LATEST,則讀取所有遠程倉庫的元數據groupId/artifactId/version/maven-metadata.xml,與本地元數據合並后,計算出RELEASE或LATEST的真實值,然后基於真實值檢查本地倉庫和遠程倉庫,如步驟(2)(3);
(5)如果依賴的版本為SNAPSHOT,類似的,讀取遠程倉庫的元數據,並與本地元數據合並,計算出最新版本的快照,再從本地倉庫和遠程倉庫檢索。
下邊為一個快照版本依賴的元數據maven-metadata-local.xml,包括最近更新時間戳,以及存在的不同版本:
<?xml version="1.0" encoding="UTF-8"?> <metadata> <groupId>com.test</groupId> <artifactId>C</artifactId> <versioning> <versions> <version>1.0-SNAPSHOT</version> <version>2.0-SNAPSHOT</version> </versions> <lastUpdated>20171113125841</lastUpdated> </versioning> </metadata>
對應文件目錄結構如下:
其中每個版本中,包含對自身描述的元數據。以2.0-SNAPSHOT為例,其目錄結構如下,主要包含jar和pom兩部分,maven-metadata-local.xml為依賴元數據:
具體元數據內容如下,記錄了該版本依賴最近一次的更新時間。在本地庫中沒有該構件的時候,會檢索所有遠程倉庫,結合元數據文件,計算最新的版本;或者在進行強制更新的時候,也會計算出最新版本。
<?xml version="1.0" encoding="UTF-8"?> <metadata modelVersion="1.1.0"> <groupId>com.test</groupId> <artifactId>C</artifactId> <version>2.0-SNAPSHOT</version> <versioning> <snapshot> <localCopy>true</localCopy> </snapshot> <lastUpdated>20171117132322</lastUpdated> <snapshotVersions> <snapshotVersion> <extension>jar</extension> <value>2.0-SNAPSHOT</value> <updated>20171117132322</updated> </snapshotVersion> <snapshotVersion> <extension>pom</extension> <value>2.0-SNAPSHOT</value> <updated>20171117132322</updated> </snapshotVersion> </snapshotVersions> </versioning> </metadata>
3. 依賴傳遞性解析實例
創建3個Maven工程A、B、C(命令mvn archetype:generate)。
(1)編輯各自的POM文件,使其依賴關系如下圖所示:
其中B為A的直接依賴,C具有傳遞性,為A的傳遞性依賴,分析其依賴樹可以直觀的看出依賴關系(mvn dependency:tree),如下:
- B依賴於C(1.0)
- A依賴於B(1.0),間接依賴C(1.0)
(2)將B對的C的依賴改為optional,即可選的,此時A的依賴樹如下,不包括C:
(3)修改POM文件,使其依賴關系如下:
看A的依賴樹,如下:
綜上,正常情況下依賴是具有傳遞性,除非聲明為optional。
總結:
- 聲明依賴時,除了常用的三項位置元素,還具有包括范圍、類型、可選和排除等;
- 依賴具有傳遞性,具有路徑優先的約定;
- 當引用多個工程時,會潛在的引用其他依賴,需要注意是否會引錯包,或者沖突;
- Maven依賴解析,對於本地庫中沒有的構件,Maven會綜合遠程倉庫與本地倉庫的元數據,計算最新版本后,再引用。
參考:
- 《Maven實踐》