Maven 3-Maven依賴版本衝突的分析及解決小結 (阿里,美團,京東面試)

舉例
A依賴於B及C,而B又依賴於X、Y,而C依賴於X、M,則A除引B及C的依賴包下,還會引入X,Y,M的依賴包(通常狀況下了,Maven可經過<scope>等若干種方式控制傳遞依賴)。
這裏有一個須要特別注意的,即B和C同時依賴於X,假設B依賴於X的1.0版本,而C依賴於X的2.0版本,A究竟依賴於X的1.0仍是2.0版本呢?
這就看Classloader的加載順序了,假設Classloader先加載X_1.0,而它就不會再加載X_2.0了,若是A偏偏但願使用X_2.0呢,血案就這樣不期而遇了。html

 

好比 A 依賴 版本爲2.0 的 C ,B 依賴 版本爲3.0的 C。在你的pom中,你同時依賴了 A 和 B ,這時就會產生衝突。這時候你就要判斷,哪一個版本能同時讓A和B工做(若是能夠的話),而後排除掉另外一個就好了。我一般都是排除掉較低的版本。 java

<dependencies>  
        <dependency>  
            <groupId>A</groupId>  
            <artifactId>A</artifactId>  
            <version>xxx</version>  
            <exclusions>  
                <exclusion>  
                    <groupId>C</groupId>  
                    <artifactId>C</artifactId>  
                </exclusion>  
            </exclusions>  
        </dependency>  
        <dependency>  
            <groupId>B</groupId>  
            <artifactId>B</artifactId>              
        </dependency>  
</dependencies>

 

理包依賴是 Maven 核心功能之一,下面經過如何引入 jar 包;如何解析 jar 包依賴;包衝突是如何產生;如何解決包衝突;依賴管理解決什麼問題;什麼是依賴範圍;使用包依賴的最佳實踐等 6 個問題來介紹。web

如何引入 jar 包

在代碼開發時,若是須要使用第三方 jar 包提供的類庫,那麼須要在 pom.xml 加入該 jar 包依賴。 例如:使用 zookeeper clientspring

<dependencies>
  <!-- https://mvnrepository.com/artifact/org.apache.hadoop/zookeeper -->
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>zookeeper</artifactId>
      <version>3.3.1</version>
  </dependency>
</dependencies>

 

Maven 如何解析 jar 包依賴——傳遞依賴

如上所述,在 pom.xml 中引入 zookeeper jar 包依賴,當 Maven 解析該依賴時,須要引入的 jar 包不只僅只有 zookeeper,還會有 zookeeper 內部依賴的 jar 包,還會有 zookeeper 內部依賴的 jar 包依賴的 jar 包......,依賴關係不斷傳遞,直至沒有依賴。
例如:上述 pom.xml 引入 zookeeper 依賴,實際引入的 jar 包有apache

包衝突如何產生?

舉個�:假設 A->B->C->D1, E->F->D2,D1,D2 分別爲 D 的不一樣版本。
若是 pom.xml 文件中引入了 A 和 E 以後,按照 Maven 傳遞依賴原則,工程內須要引入的實際 Jar 包將會有:A B C D1 和 E F D2,所以 D1,D2 將會產生包衝突。api

如何解決包衝突

Maven 解析 pom.xml 文件時,同一個 jar 包只會保留一個,這樣有效的避免因引入兩個 jar 包致使的工程運行不穩定性。緩存

Maven 默認處理策略

  • 最短路徑優先
    Maven 面對 D1 和 D2 時,會默認選擇最短路徑的那個 jar 包,即 D2。E->F->D2 比 A->B->C->D1 路徑短 1。
  • 最早聲明優先
    若是路徑同樣的話,舉個�: A->B->C1, E->F->C2 ,兩個依賴路徑長度都是 2,那麼就選擇最早聲明。

移除依賴

若是咱們不想經過 A->B->->D1 引入 D1 的話,那麼咱們在聲明引入 A 的時候將 D1 排除掉,這樣也避免了包衝突。
舉個�:將 zookeeper 的 jline 依賴排除 用exclusions標籤app

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.3.1</version>
    <exclusions>
        <exclusion>
            <groupId>jline</groupId>
            <artifactId>jline</artifactId>
        </exclusion>
    </exclusions>
</dependency>

 

檢測包衝突工具

mvn dependency:help

mvn dependency:analyze

mvn dependency:tree

mvn dependency:tree -Dverbose

詳細參考:mvn dependency
mvn dependency:treemaven

依賴管理解決什麼問題

當同一個工程內有多個模塊時,而且要求多個模塊使用某個 jar 包的相同版本,爲了方便統一版本號,升級版本號,須要提取出一個父親模塊來管理子模塊共同依賴的 jar 包版本。
舉個�:有兩個模塊 projectA, projectB,它們的依賴分別以下所示:
projectA:ide

<project> ... <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>
</project>

 

projectB:

<project> ... <dependencies>
    <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>
</project>

 

projectA 和 projectB 共同依賴了 group-a/artifact-b/1.0,提取公共依賴,生成 parent, parent 依賴以下:

<project> ... <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>group-a</groupId>
        <artifactId>artifact-b</artifactId>
        <version>1.0</version>
        <type>bar</type>
        <scope>runtime</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

 

則 projectA 和 projectB 均不須要指定 group-a/artifact-b 的 version 信息,將來升級 version 信息時,只須要在 parent 內部指定。

projectA:

<project> ... <parent>
<groupId>group-a</groupId>
        <artifactId>artifact-b</artifactId>
        <version>1.0</version>
</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>
    </dependency>
  </dependencies>
</project>

 

projectB:

<project> ... <dependencies>
    <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>
    </dependency>
  </dependencies>
</project>

 

依賴範圍

若是不顯示執行 <scope> 屬性時,默認 <scope>compile</scope>。
scope 有哪些屬性:compile, provided, runtime, test, system 等。
詳細參考:依賴範圍

最佳實踐

(1)項目中源代碼使用的 jar 包必定在 pom.xml 中顯示引用。
(2)常常 check 一下包衝突,檢查是否須要處理。
(3)當使用多個模塊時,parent 必定要使用包管理模塊來規範 Jar 包版本,而不是包依賴模塊直接引入依賴。 dependencyManagement vs dependencies

 

 

 



 第二個問題:
第一板斧:找到傳遞依賴的鬼出在哪裏?

dependency:tree是把照妖照,pom.xml用它照照,全部傳遞性依賴都將無處遁形,而且會以層級樹方式展示,很是直觀。

如下就是執行dependency:tree後的一個輸出:
引用

[INFO] --- maven-dependency-plugin:2.1:tree (default-cli) @ euler-foundation --- [INFO] com.hsit:euler-foundation:jar:0.9.0.1-SNAPSHOT [INFO] +- com.rop:rop:jar:1.0.1:compile [INFO] |  +- org.slf4j:slf4j-api:jar:1.7.5:compile [INFO] |  +- org.slf4j:slf4j-log4j12:jar:1.7.5:compile [INFO] |  +- log4j:log4j:jar:1.2.16:compile [INFO] |  +- commons-lang:commons-lang:jar:2.6:compile [INFO] |  +- commons-codec:commons-codec:jar:1.6:compile [INFO] |  +- javax.validation:validation-api:jar:1.0.0.GA:compile [INFO] |  +- org.hibernate:hibernate-validator:jar:4.2.0.Final:compile [INFO] |  +- org.codehaus.jackson:jackson-core-asl:jar:1.9.5:compile [INFO] |  +- org.codehaus.jackson:jackson-mapper-asl:jar:1.9.5:compile [INFO] |  +- org.codehaus.jackson:jackson-jaxrs:jar:1.9.5:compile [INFO] |  +- org.codehaus.jackson:jackson-xc:jar:1.9.5:compile [INFO] |  \- com.fasterxml.jackson.dataformat:jackson-dataformat-xml:jar:2.2.3:compile [INFO] |     +- com.fasterxml.jackson.core:jackson-core:jar:2.2.3:compile [INFO] |     +- com.fasterxml.jackson.core:jackson-annotations:jar:2.2.3:compile [INFO] |     +- com.fasterxml.jackson.core:jackson-databind:jar:2.2.3:compile [INFO] |     +- com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:2.2.3:compile [INFO] |     \- org.codehaus.woodstox:stax2-api:jar:3.1.1:compile [INFO] |        \- javax.xml.stream:stax-api:jar:1.0-2:compile

 




剛纔吹噓dependency:tree時,我用到了「無處遁形」,其實有時你會發現簡單地用dependency:tree每每並不能查看到全部的傳遞依賴。不過若是你真的想要看全部的,必須得加一個-Dverbose參數,這時就一定是最全的了。
全是全了,但顯示出來的東西太多,頭暈目眩,有沒有好法呢?固然有了,加上Dincludes或者Dexcludes說出你喜歡或討厭,dependency:tree就會幫你過濾出來:
引用

Dincludes=org.springframework:spring-tx

 


過濾串使用groupId:artifactId:version的方式進行過濾,能夠不寫全啦,如:

mvn dependency:tree -Dverbose -Dincludes=asm:asm

 



就會出來asm依賴包的分析信息:

[INFO] --- maven-dependency-plugin:2.1:tree (default-cli) @ ridge-test --- [INFO] com.ridge:ridge-test:jar:1.0.2-SNAPSHOT [INFO] +- asm:asm:jar:3.2:compile [INFO] \- org.unitils:unitils-dbmaintainer:jar:3.3:compile [INFO] \- org.hibernate:hibernate:jar:3.2.5.ga:compile [INFO] +- cglib:cglib:jar:2.1_3:compile [INFO] |  \- (asm:asm:jar:1.5.3:compile - omitted for conflict with 3.2) [INFO] \- (asm:asm:jar:1.5.3:compile - omitted for conflict with 3.2) [INFO] ------------------------------------------------------------------------

 



對asm有依賴有一個直接的依賴(asm:asm:jar:3.2)還有一個傳遞進入的依賴(asm:asm:jar:1.5.3)

第二板斧:將不想要的傳遞依賴剪除掉

承上,假設咱們不但願asm:asm:jar:1.5.3出現,根據分析,咱們知道它是經由org.unitils:unitils-dbmaintainer:jar:3.3引入的,那麼在pom.xml中找到這個依賴,作其它的調整:

    

<dependency>  
        <groupId>org.unitils</groupId>  
        <artifactId>unitils-dbmaintainer</artifactId>  
        <version>${unitils.version}</version>  
        <exclusions>  
            <exclusion>  
                <artifactId>dbunit</artifactId>  
                <groupId>org.dbunit</groupId>  
            </exclusion>  
            <!-- 這個就是咱們要加的片段 -->  
            <exclusion>  
                <artifactId>asm</artifactId>  
                <groupId>asm</groupId>  
            </exclusion>  
        </exclusions>  
    </dependency>

 




再分析一下,你能夠看到傳遞依賴沒有了:


 [INFO] [INFO] --- maven-dependency-plugin:2.1:tree (default-cli) @ ridge-test --- [INFO] com.ridge:ridge-test:jar:1.0.2-SNAPSHOT [INFO] \- asm:asm:jar:3.2:compile [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS 

 





第三板斧:查看運行期類來源的JAR包

有時,你覺得解決了,可是恰恰仍是報類包衝突(典型症狀是java.lang.ClassNotFoundException或Method不兼容等異常),這時你能夠設置一個斷點,在斷點處經過下面這個我作的工具類來查看Class所來源的JAR包:

   

package com.ridge.util; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.security.CodeSource; import java.security.ProtectionDomain; /** * @author : chenxh * @date: 13-10-31 */  
    public class ClassLocationUtils { /** * 獲取類全部的路徑 * @param cls * @return 
         */  
        public static String where(final Class cls) { if (cls == null)throw new IllegalArgumentException("null input: cls"); URL result = null; final String clsAsResource = cls.getName().replace('.', '/').concat(".class"); final ProtectionDomain pd = cls.getProtectionDomain(); if (pd != null) { final CodeSource cs = pd.getCodeSource(); if (cs != null) result = cs.getLocation(); if (result != null) { if ("file".equals(result.getProtocol())) { try { if (result.toExternalForm().endsWith(".jar") || result.toExternalForm().endsWith(".zip")) result = new URL("jar:".concat(result.toExternalForm()) .concat("!/").concat(clsAsResource)); else if (new File(result.getFile()).isDirectory()) result = new URL(result, clsAsResource); } catch (MalformedURLException ignore) {} } } } if (result == null) { final ClassLoader clsLoader = cls.getClassLoader(); result = clsLoader != null ? clsLoader.getResource(clsAsResource) : ClassLoader.getSystemResource(clsAsResource); } return result.toString(); } } 

 




隨便寫一個測試,設置好斷點,在執行到斷點處按alt+F8動態執行代碼(intelij idea),假設咱們輸入:

ClassLocationUtils.where(org.objectweb.asm.ClassVisitor.class)

 



便可立刻查出類對應的JAR了:

這就是org.objectweb.asm.ClassVisitor類在運行期對應的JAR包,若是這個JAR包版本不是你指望你,就說明是你的IDE緩存形成的,這時建議你Reimport一下maven列表就能夠了,以下所示(idea):

Reimport一下,IDE會強制根據新的pom.xml設置從新分析並加載依賴類包,以獲得和pom.xml設置相同的依賴。(這一步很是重要哦,常常項目組pom.xml是相同的,可是就是有些人能夠運行,有些人不能運行,俗稱人品問題,其實都是IDE的緩存形成的了
idea清除緩存,爲了提升效率不建議採用reimport從新起開啓項目的方式,建議採用idea自帶的功能,File->Invalidate Caches 功能直接完成清除idea cache

 

 

 

 

 

 

3、另外一個問題,log衝突:項目中出現的問題以下:

 

Caused by: java.lang.NoClassDefFoundError: Could not initialize class org.apache.log4j.Log4jLoggerFactory

 

後經網上搜索加邊上大牛指點發現:

log4j-over-slf4j.jar 和 slf4j-log4j12.jar 在同一個classpath下就會出現這個錯誤。

解決方法: 
將slf4j-log4j12.jar從相關的jar中排除

可是查看maven項目中的pom文件,本身並無配置這個jar的依賴,猜想是maven加載其餘jar引入的依賴包。

打開pom.xml文件,在Dependency Hierarchy(依賴列表)中查看jar包的依賴層次關係。

 

在過濾欄中輸入log4j,右側出現了log4j相關包的依賴結構,左側則是pom.xml所有依賴包的列表展現。

直接在右側選中zookeeper底下的slf4j的jar包,右鍵選擇Exclude,而後保存pom.xml。這樣在加載zookeeper的jar包時就不會再加載slf4j的jar包。

修改後對應的dependency文件以下:

 

 
<dependency>  
    <groupId>org.apache.zookeeper</groupId>  
    <artifactId>zookeeper</artifactId>  
    <version>3.4.6</version>  
    <exclusions>  
        <exclusion>  
            <artifactId>slf4j-log4j12</artifactId>  
            <groupId>org.slf4j</groupId>  
        </exclusion>  
    </exclusions>  
</dependency>

 


這樣就能經過filter過濾快速找到對應jar,並知道他的依賴關係,快速解決項目中的jar包衝突問題。 

參考:Maven類包衝突終極解決方案

參考:利用maven工具解決jar包衝突問題或重複加載問題

參考:Maven 解決jar包衝突的原理

相關文章
相關標籤/搜索