本文從實際工作中的一個bug出發,講解了業務的背景、分析了問題產生的原因、介紹瞭解決問題的思路,同時介紹了Maven的依賴機制。
業務場景
最近在工作中,使用Dubbo呼叫遠端服務,需要依賴被呼叫方(dubbo service provider)提供的一些jar包。
下面是maven和dubbo的相關配置。
- pom.xml
<!-- 遠端dubbo服務 -->
<dependency>
<groupId>com.dubbo.service.provider</groupId>
<artifactId>foo-api</artifactId>
<version>1.0</version>
</dependency>
複製程式碼
- dubbo-serivce-provider.xml
<dubbo:reference id="fooService" interface="com.dubbo.service.provider.FooService" check="false" url="${dubbo.foo.server.address}"/>
複製程式碼
發現問題
專案啟動後,出現如下異常
java.lang.NoSuchMethodError: com.google.common.base.Platform.systemNanoTime()
複製程式碼
問題原因
通過Eclipse檢視依賴樹發現,foo-api所依賴的jar與專案中的jar發生了衝突。
可以將如上場景抽象為下面的邏輯:
A依賴
-> B
D依賴
-> A
-> B
複製程式碼
因為Maven擁有傳遞依賴的特性,因此真實的依賴樹是:
A依賴
-> B
D依賴
-> A
-> B
-> B
複製程式碼
因此D專案發生了依賴衝突。
相關知識:依賴傳遞(Transitive Dependencies)
依賴傳遞(Transitive Dependencies)是Maven 2.0開始的提供的特性,依賴傳遞的好處是不言而喻的,可以讓我們不需要去尋找和發現所必須依賴的庫,而是將會自動將需要依賴的庫幫我們加進來。
例如A依賴了B,B依賴了C和D,那麼你就可以在A中,像主動依賴了C和D一樣使用它們。並且傳遞的依賴是沒有數量和層級的限制的,非常方便。
但依賴傳遞也不可避免的會帶來一些問題,例如:
- 當依賴層級很深的時候,可能造成迴圈依賴(cyclic dependency)
- 當依賴的數量很多的時候,依賴樹會非常大
針對這些問題,Maven提供了很多管理依賴的特性:
依賴調節(Dependency mediation)
依賴調節是為了解決版本不一致的問題(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)
通過宣告Dependency management,可以大大簡化子POM的依賴宣告。
舉例來說專案A,B,C,D都有共同的Parent,並有類似的依賴宣告如下:
- A、B、C、D/pom.xml
<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:
複製程式碼
- Parent/pom.xml
<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>
複製程式碼
那麼子專案的依賴宣告會非常簡單:
- A、B、C、D/pom.xml
<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>
複製程式碼
依賴範圍(Dependency scope)
Maven在編譯主程式碼的時候需要使用一套classpath,在編譯和執行測試的時候會使用另一套classpath,實際執行專案的時候,又會使用一套classpath。
依賴範圍就是用來控制依賴與這三種classpath(編譯classpath、測試classpath、執行classpath)的關係的,Maven有以下幾種依賴範圍:
- compile: 編譯依賴範圍。
如果沒有指定,就會預設使用該依賴範圍。
使用此依賴範圍的Maven依賴,對於編譯、測試、執行三種classpath都有效。
- test: 測試依賴範圍。
使用此依賴範圍的Maven依賴,只對於測試classpath有效,在編譯主程式碼或者執行專案的使用時將無法使用此類依賴。
典型例子是JUnit,它只有在編譯測試程式碼及執行測試的時候才需要。
- provided: 已提供依賴範圍。
使用此依賴範圍的Maven依賴,對於編譯和測試classpath有效,但在執行時無效。
典型例子是servlet-api,編譯和測試專案的時候需要該依賴,但在執行專案的時候,由於容器已經提供,就不需要Maven重複地引入一遍。
- runtime: 執行時依賴範圍。
使用此依賴範圍的Maven依賴,對於測試和執行classpath有效,但在編譯主程式碼時無效。
典型例子是JDBC驅動實現,專案主程式碼的編譯只需要JDK提供的JDBC介面,只有在執行測試或者執行專案的時候才需要實現上述介面的具體JDBC驅動。
- system: 系統依賴範圍。
該依賴與三種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>
複製程式碼
- import(Maven 2.0.9及以上): 匯入依賴範圍。
我們知道,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裡面
排除依賴(Excluded dependencies)
排除不需要從所依賴的專案中傳遞過來的依賴,好比你買車的時候,主動跟賣車的說明不需要買車附加的保險業務。下面在解決思路中會舉例說明。
可選依賴(Optional dependencies)
被依賴的專案主動不把可以傳遞的依賴傳遞下去,好比賣車的主動宣告自己不會讓買車的人買這輛車附加的保險業務。下面在解決思路中會舉例說明。
解決思路
有了上面的知識背景,考慮使用Maven提供的Optional和Exclusions來控制依賴的傳遞。
A
-> B
D
-> A
-> B
複製程式碼
Optional 定義後,該依賴只能在本專案中傳遞,不會傳遞到引用該專案的父專案中,父專案需要主動引用該依賴才行。
- A/pom.xml
<dependency>
<groupId>com.bar</groupId>
<artifactId>B</artifactId>
<version>1.0</version>
<optional>true</optional>
</dependency>
複製程式碼
這種情況下,A對B的依賴將不會傳遞給D.
Exclusions 則是主動排除子專案傳遞過來的依賴。
- D/pom.xml
<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非常必要。
—— 拉斐爾《雅典學院》