Maven依賴管理:控制依賴的傳遞

西召發表於2019-03-02

本文從實際工作中的一個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()
複製程式碼

image

問題原因

通過Eclipse檢視依賴樹發現,foo-api所依賴的jar與專案中的jar發生了衝突。

image

可以將如上場景抽象為下面的邏輯:

  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非常必要。

拉斐爾《雅典學院》

—— 拉斐爾《雅典學院》

相關文章