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

本文從實際工做中的一個bug出發,講解了業務的背景、分析了問題產生的緣由、介紹瞭解決問題的思路,同時介紹了Maven的依賴機制。java

業務場景

最近在工做中,使用Dubbo調用遠程服務,須要依賴被調用方(dubbo service provider)提供的一些jar包。api

下面是maven和dubbo的相關配置。bash

  • 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}"/>
複製代碼

發現問題

項目啓動後,出現以下異常maven

java.lang.NoSuchMethodError: com.google.common.base.Platform.systemNanoTime()
複製代碼

image

問題緣由

經過Eclipse查看依賴樹發現,foo-api所依賴的jar與項目中的jar發生了衝突。ide

image

能夠將如上場景抽象爲下面的邏輯:測試

A依賴
      -> B

  D依賴
      -> A
      -> B
複製代碼

由於Maven擁有傳遞依賴的特性,所以真實的依賴樹是:ui

A依賴
      -> B

  D依賴
      -> A
          -> B
      -> B
複製代碼

所以D項目發生了依賴衝突。google

相關知識:依賴傳遞(Transitive Dependencies)

依賴傳遞(Transitive Dependencies)是Maven 2.0開始的提供的特性,依賴傳遞的好處是不言而喻的,可讓咱們不須要去尋找和發現所必須依賴的庫,而是將會自動將須要依賴的庫幫咱們加進來。url

例如A依賴了B,B依賴了C和D,那麼你就能夠在A中,像主動依賴了C和D同樣使用它們。而且傳遞的依賴是沒有數量和層級的限制的,很是方便。spa

但依賴傳遞也不可避免的會帶來一些問題,例如:

  • 當依賴層級很深的時候,可能形成循環依賴(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很是必要。

拉斐爾《雅典學院》

—— 拉斐爾《雅典學院》

相關文章
相關標籤/搜索