本文從實際工做中的一個bug出發,講解了業務的背景、分析了問題產生的緣由、介紹瞭解決問題的思路,同時介紹了Maven的依賴機制。java
最近在工做中,使用Dubbo調用遠程服務,須要依賴被調用方(dubbo service provider)提供的一些jar包。api
下面是maven和dubbo的相關配置。bash
<!-- 遠程dubbo服務 -->
<dependency>
<groupId>com.dubbo.service.provider</groupId>
<artifactId>foo-api</artifactId>
<version>1.0</version>
</dependency>
複製代碼
<dubbo:reference id="fooService" interface="com.dubbo.service.provider.FooService" check="false" url="${dubbo.foo.server.address}"/>
複製代碼
項目啓動後,出現以下異常maven
java.lang.NoSuchMethodError: com.google.common.base.Platform.systemNanoTime()
複製代碼
經過Eclipse查看依賴樹發現,foo-api所依賴的jar與項目中的jar發生了衝突。ide
能夠將如上場景抽象爲下面的邏輯:測試
A依賴
-> B
D依賴
-> A
-> B
複製代碼
由於Maven擁有傳遞依賴的特性,所以真實的依賴樹是:ui
A依賴
-> B
D依賴
-> A
-> B
-> B
複製代碼
所以D項目發生了依賴衝突。google
依賴傳遞(Transitive Dependencies)是Maven 2.0開始的提供的特性,依賴傳遞的好處是不言而喻的,可讓咱們不須要去尋找和發現所必須依賴的庫,而是將會自動將須要依賴的庫幫咱們加進來。url
例如A依賴了B,B依賴了C和D,那麼你就能夠在A中,像主動依賴了C和D同樣使用它們。而且傳遞的依賴是沒有數量和層級的限制的,很是方便。spa
但依賴傳遞也不可避免的會帶來一些問題,例如:
針對這些問題,Maven提供了不少管理依賴的特性:
依賴調節是爲了解決版本不一致的問題(multiple versions),並採起就近原則(nearest definition)。
舉例來講,A項目經過依賴傳遞依賴了兩個版本的D:
A -> B -> C -> ( D 2.0) , A -> E -> (D 1.0),
那麼最終A依賴的D的version將會是1.0,由於1.0對應的層級更少,也就是更近。
經過聲明Dependency management,能夠大大簡化子POM的依賴聲明。
舉例來講項目A,B,C,D都有共同的Parent,並有相似的依賴聲明以下:
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>group-c</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
複製代碼
若是父pom聲明瞭以下的Dependency management:
複製代碼
<dependencyManagement>
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>group-c</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</dependencyManagement>
複製代碼
那麼子項目的依賴聲明會很是簡單:
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<!-- 依賴的類型,對應於項目座標定義的packaging。大部分狀況下,該元素沒必要聲明,其默認值是jar.-->
<type>bar</type>
</dependency>
</dependencies>
複製代碼
Maven在編譯主代碼的時候須要使用一套classpath,在編譯和執行測試的時候會使用另外一套classpath,實際運行項目的時候,又會使用一套classpath。
依賴範圍就是用來控制依賴與這三種classpath(編譯classpath、測試classpath、運行classpath)的關係的,Maven有如下幾種依賴範圍:
若是沒有指定,就會默認使用該依賴範圍。
使用此依賴範圍的Maven依賴,對於編譯、測試、運行三種classpath都有效。
使用此依賴範圍的Maven依賴,只對於測試classpath有效,在編譯主代碼或者運行項目的使用時將沒法使用此類依賴。
典型例子是JUnit,它只有在編譯測試代碼及運行測試的時候才須要。
使用此依賴範圍的Maven依賴,對於編譯和測試classpath有效,但在運行時無效。
典型例子是servlet-api,編譯和測試項目的時候須要該依賴,但在運行項目的時候,因爲容器已經提供,就不須要Maven重複地引入一遍。
使用此依賴範圍的Maven依賴,對於測試和運行classpath有效,但在編譯主代碼時無效。
典型例子是JDBC驅動實現,項目主代碼的編譯只須要JDK提供的JDBC接口,只有在執行測試或者運行項目的時候才須要實現上述接口的具體JDBC驅動。
該依賴與三種classpath的關係,和provided依賴範圍徹底一致。但使用system範圍依賴時必須經過systemPath元素顯式地指定依賴文件的路徑。因爲此類依賴不是經過Maven倉庫解析的,並且每每與本機系統綁定,可能形成構建的不可移植,所以應該謹慎使用。
systemPath元素能夠引用環境變量:
<dependency>
<groupId>com.system</groupId>
<artifactId>foo</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${maven.home}/lib/foo.jar</systemPath>
</dependency>
複製代碼
咱們知道,maven的繼承和java是同樣的,只能單繼承。所以,父pom可能很是龐大,若是你想把依賴分類清晰的進行管理,就更不可能了。
import scope依賴能解決這個問題。你能夠把Dependency Management放到單獨用來管理依賴的pom中,而後在須要使用依賴的模塊中經過import scope依賴,就能夠引入dependencyManagement。
例如,父pom.xml:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.test.sample</groupId>
<artifactId>base-parent1</artifactId>
<packaging>pom</packaging>
<version>1.0.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
<version>4.8.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
複製代碼
經過非繼承的方式來引入這段依賴管理配置:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test.sample</groupId>
<artifactid>base-parent1</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
</dependency>
複製代碼
注意:import scope只能用在dependencyManagement裏面
排除不須要從所依賴的項目中傳遞過來的依賴,比如你買車的時候,主動跟賣車的說明不須要買車附加的保險業務。下面在解決思路中會舉例說明。
被依賴的項目主動不把能夠傳遞的依賴傳遞下去,比如賣車的主動聲明本身不會讓買車的人買這輛車附加的保險業務。下面在解決思路中會舉例說明。
有了上面的知識背景,考慮使用Maven提供的Optional和Exclusions來控制依賴的傳遞。
A
-> B
D
-> A
-> B
複製代碼
Optional 定義後,該依賴只能在本項目中傳遞,不會傳遞到引用該項目的父項目中,父項目須要主動引用該依賴才行。
<dependency>
<groupId>com.bar</groupId>
<artifactId>B</artifactId>
<version>1.0</version>
<optional>true</optional>
</dependency>
複製代碼
這種狀況下,A對B的依賴將不會傳遞給D.
Exclusions 則是主動排除子項目傳遞過來的依賴。
<dependency>
<groupId>com.bar</groupId>
<artifactId>A</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>com.bar</groupId>
<artifactId>B</artifactId>
</exclusion>
</exclusions>
</dependency>
複製代碼
這種狀況下,D對A的依賴將不會包含B.
開始提到的問題就是經過exclusion的方式解決的。
Maven的依賴機制(Dependency Mechanism)是Maven最著名的特性,而且是Maven在依賴管理領域中最使人稱道的。所以,對Maven的依賴機制有深刻的理解,對使用Maven很是必要。
—— 拉斐爾《雅典學院》