從新看待Jar包衝突問題及解決方案

  Jar包衝突是老生常談的問題,幾乎每個Java程序猿都不可避免地遇到過,而且也都能想到一般的緣由通常是同一個Jar包因爲maven傳遞依賴等緣由被引進了多個不一樣的版本而致使,可採用依賴排除、依賴管理等常規方式來嘗試解決該問題,但這些方式真正能完全解決該衝突問題嗎?答案是否認的。筆者之因此將文章題目起爲「從新看待」,是由於以前對於Jar包衝突問題的理解僅僅停留在前面所說的那些,直到在工做中遇到的一系列Jar包衝突問題後,才發現並非那麼簡單,對該問題有了從新的認識,接下來本文將圍繞Jar包衝突的問題本質和相關的解決方案這兩個點進行闡述。html

Jar包衝突問題

1、衝突的本質

  Jar包衝突的本質是什麼?Google了半天也沒找到一個讓人滿意的完整定義。其實,咱們能夠從Jar包衝突產生的結果來總結,在這裏給出以下定義(此處若有不妥,歡迎拍磚~-~):java

Java應用程序因某種因素,加載不到正確的類而致使其行爲跟預期不一致。web

  具體來講可分爲兩種狀況:1)應用程序依賴的同一個Jar包出現了多個不一樣版本,並選擇了錯誤的版本而致使JVM加載不到須要的類或加載了錯誤版本的類,爲了敘述的方便,筆者稱之爲第一類Jar包衝突問題;2)一樣的類(類的全限定名徹底同樣)出如今多個不一樣的依賴Jar包中,即該類有多個版本,並因爲Jar包加載的前後順序致使JVM加載了錯誤版本的類,稱之爲第二類Jar包問題。這兩種狀況所致使的結果實際上是同樣的,都會使應用程序加載不到正確的類,那其行爲天然會跟預期不一致了,如下對這兩種類型進行詳細分析。spring

1.1 同一個Jar包出現了多個不一樣版本

  隨着Jar包迭代升級,咱們所依賴的開源的或公司內部的Jar包工具都會存在若干不一樣的版本,而版本升級天然就避免不了類的方法簽名變動,甚至於類名的更替,而咱們當前的應用程序每每依賴特定版本的某個類 M ,因爲maven的傳遞依賴而致使同一個Jar包出現了多個版本,當maven的仲裁機制選擇了錯誤的版本時,而剛好類 M在該版本中被去掉了,或者方法簽名改了,致使應用程序因找不到所需的類 M或找不到類 M中的特定方法,就會出現第一類Jar衝突問題。可總結出該類衝突問題發生的如下三個必要條件:數據庫

  • 因爲maven的傳遞依賴致使依賴樹中出現了同一個Jar包的多個版本
  • 該Jar包的多個版本之間存在接口差別,如類名更替,方法簽名更替等,且應用程序依賴了其中有變動的類或方法
  • maven的仲裁機制選擇了錯誤的版本

1.2 同一個類出如今多個不一樣Jar包中

  一樣的類出如今了應用程序所依賴的兩個及以上的不一樣Jar包中,這會致使什麼問題呢?咱們知道,同一個類加載器對於同一個類只會加載一次(多個不一樣類加載器就另說了,這也是解決Jar包衝突的一個思路,後面會談到),那麼當一個類出如今了多個Jar包中,假設有 ABC 等,因爲Jar包依賴的路徑長短、聲明的前後順序或文件系統的文件加載順序等緣由,類加載器首先從Jar包 A 中加載了該類後,就不會加載其他Jar包中的這個類了,那麼問題來了:若是應用程序此時須要的是Jar包 B 中的類版本,而且該類在Jar包 AB 中有差別(方法不一樣、成員不一樣等等),而JVM卻加載了Jar包 A 的中的類版本,與指望不一致,天然就會出現各類詭異的問題。apache

  從上面的描述中,能夠發現出現不一樣Jar包的衝突問題有如下三個必要條件:bootstrap

  • 同一個類 M 出如今了多個依賴的Jar包中,爲了敘述方便,假設仍是兩個: AB
  • Jar包 AB 中的該類 M 有差別,不管是方法簽名不一樣也好,成員變量不一樣也好,只要能夠形成實際加載的類的行爲和指望不一致都行。若是說Jar包 AB 中的該類徹底同樣,那麼類加載器不管先加載哪一個Jar包,獲得的都是一樣版本的類 M ,不會有任何影響,也就不會出現Jar包衝突帶來的詭異問題。
  • 加載的類 M 不是所指望的版本,即加載了錯誤的Jar包

2、衝突的產生緣由  

2.1 maven仲裁機制

  當前maven大行其道,說到第一類Jar包衝突問題的產生緣由,就不得不提maven的依賴機制了。傳遞性依賴是Maven2.0引入的新特性,讓咱們只需關注直接依賴的Jar包,對於間接依賴的Jar包,Maven會經過解析從遠程倉庫獲取的依賴包的pom文件來隱式地將其引入,這爲咱們開發帶來了極大的便利,但與此同時,也帶來了常見的問題——版本衝突,即同一個Jar包出現了多個不一樣的版本,針對該問題Maven也有一套仲裁機制來決定最終選用哪一個版本,但Maven的選擇每每不必定是咱們所指望的,這也是產生Jar包衝突最多見的緣由之一。先來看下Maven的仲裁機制:tomcat

  • 優先按照依賴管理**<dependencyManagement>**元素中指定的版本聲明進行仲裁,此時下面的兩個原則都無效了
  • 若無版本聲明,則按照「短路徑優先」的原則(Maven2.0)進行仲裁,即選擇依賴樹中路徑最短的版本
  • 若路徑長度一致,則按照「第一聲明優先」的原則進行仲裁,即選擇POM中最早聲明的版本

  從maven的仲裁機制中能夠發現,除了第一條仲裁規則(這也是解決Jar包衝突的經常使用手段之一)外,後面的兩條原則,對於同一個Jar包不一樣版本的選擇,maven的選擇有點「一廂情願」了,也許這是maven研發團隊在總結了大量的項目依賴管理經驗後得出的兩條結論,又或者是發現根本找不到一種統一的方式來知足全部場景以後的無奈之舉,可能這對於多數場景是適用的,可是它不必定適合我——當前的應用,由於每一個應用都有其特殊性,該依賴哪一個版本,maven沒辦法幫你徹底搞定,若是你沒有規規矩矩地使用**<dependencyManagement>**來進行依賴管理,就註定了逃脫不了第一類Jar包衝突問題。app

2.1 Jar包的加載順序

  對於第二類Jar包衝突問題,即多個不一樣的Jar包有類衝突,這相對於第一類問題就顯得更爲棘手。爲何這麼說呢?在這種狀況下,兩個不一樣的Jar包,假設爲 AB,它們的名稱互不相同,甚至可能徹底不沾邊,若是不是出現衝突問題,你可能都不會發現它們有共有的類!對於A、B這兩個Jar包,maven就顯得無能爲力了,由於maven只會爲你針對同一個Jar包的不一樣版本進行仲裁,而這倆是屬於不一樣的Jar包,超出了maven的依賴管理範疇。此時,當A、B都出如今應用程序的類路徑下時,就會存在潛在的衝突風險,即A、B的加載前後順序就決定着JVM最終選擇的類版本,若是選錯了,就會出現詭異的第二類衝突問題。eclipse

  那麼Jar包的加載順序都由哪些因素決定的呢?具體以下:

  • Jar包所處的加載路徑,或者換個說法就是加載該Jar包的類加載器在JVM類加載器樹結構中所處層級。因爲JVM類加載的雙親委派機制,層級越高的類加載器越先加載其加載路徑下的類,顧名思義,引導類加載器(bootstrap ClassLoader,也叫啓動類加載器)是最早加載其路徑下Jar包的,其次是擴展類加載器(extension ClassLoader),再次是系統類加載器(system ClassLoader,也就是應用加載器appClassLoader),Jar包所處加載路徑的不一樣,就決定了它的加載順序的不一樣。好比咱們在eclipse中配置web應用的resin環境時,對於依賴的Jar包是添加到Bootstrap Entries中仍是User Entries中呢,則須要仔細斟酌下咯。
  • 文件系統的文件加載順序。這個因素很容易被忽略,而每每又是因環境不一致而致使各類詭異衝突問題的罪魁禍首。因tomcat、resin等容器的ClassLoader獲取加載路徑下的文件列表時是不排序的,這就依賴於底層文件系統返回的順序,那麼當不一樣環境之間的文件系統不一致時,就會出現有的環境沒問題,有的環境出現衝突。例如,對於Linux操做系統,返回順序則是由iNode的順序來決定的,若是說測試環境的Linux系統與線上環境不一致時,就極有可能出現典型案例:測試環境怎麼測都沒問題,但一上線就出現衝突問題,規避這種問題的最佳辦法就是儘可能保證測試環境與線上一致。

3、衝突的表象

  Jar包衝突可能會致使哪些問題?一般發生在編譯或運行時,主要分爲兩類問題:一類是比較直觀的也是最爲常見的錯誤是拋出各類運行時異常,還有一類就是比較隱晦的問題,它不會報錯,其表現形式是應用程序的行爲跟預期不一致,分條羅列以下:

  • java.lang.ClassNotFoundException,即java類找不到。這類典型異常一般是因爲,沒有在依賴管理中聲明版本,maven的仲裁的時候選取了錯誤的版本,而這個版本缺乏咱們須要的某個class而致使該錯誤。例如httpclient-4.4.jar升級到httpclient-4.36.jar時,類org.apache.http.conn.ssl.NoopHostnameVerifier被去掉了,若是此時咱們原本須要的是4.4版本,且用到了NoopHostnameVerifier這個類,而maven仲裁時選擇了4.6,則會致使ClassNotFoundException異常。
  • java.lang.NoSuchMethodError,即找不到特定方法,第一類衝突和第二類衝突均可能致使該問題——加載的類不正確。如果第一類衝突,則是因爲錯誤版本的Jar包與所須要版本的Jar包中的類接口不一致致使,例如antlr-2.7.2.jar升級到antlr-2.7.6.Jar時,接口antlr.collections.AST.getLine()發生變更,當maven仲裁選擇了錯誤版本而加載了錯誤版本的類AST,則會致使該異常;如果第二類衝突,則是因爲不一樣Jar包含有的同名類接口不一致致使,典型的案例:Apache的commons-lang包,2.x升級到3.x時,包名直接從commons-lang改成commons-lang3,部分接口也有所改動,因爲包名不一樣和傳遞性依賴,常常會出現兩種Jar包同時在classpath下,org.apache.commons.lang.StringUtils.isBlank就是其中有差別的接口之一,因爲Jar包的加載順序,致使加載了錯誤版本的StringUtils類,就可能出現NoSuchMethodError異常。
  • java.lang.NoClassDefFoundErrorjava.lang.LinkageError等,緣由和上述雷同,就不做具體案例分析了。
  • 沒有報錯異常,但應用的行爲跟預期不一致。這類問題一樣也是因爲運行時加載了錯誤版本的類致使,但跟前面不一樣的是,衝突的類接口都是一致的,但具體實現邏輯有差別,當咱們加載的類版本不是咱們須要的實現邏輯,就會出現行爲跟預期不一致問題。這類問題一般發生在咱們本身內部實現的多個Jar包中,因爲包路徑和類名命名不規範等問題,致使兩個不一樣的Jar包出現了接口一致但實現邏輯又各不相同的同名類,從而引起此問題。

解決方案

1、問題排查和解決

  1. 若是有異常堆棧信息,根據錯誤信息便可定位致使衝突的類名,而後在eclipse中CTRL+SHIFT+T或者在idea中CTRL+N就可發現該類存在於多個依賴Jar包中
  2. 若步驟1沒法定位衝突的類來自哪一個Jar包,可在應用程序啓動時加上JVM參數-verbose:class或者-XX:+TraceClassLoading,日誌裏會打印出每一個類的加載信息,如來自哪一個Jar包
  3. 定位了衝突類的Jar包以後,經過mvn dependency:tree -Dverbose -Dincludes=<groupId>:<artifactId>查看是哪些地方引入的Jar包的這個版本
  4. 肯定Jar包來源以後,若是是第一類Jar包衝突,則可用**<excludes>排除不須要的Jar包版本或者在依賴管理<dependencyManagement>中申明版本;如果第二類Jar包衝突,若是可排除,則用<excludes>**排掉不須要的那個Jar包,若不能排,則需考慮Jar包的升級或換個別的Jar包。固然,除了這些方法,還能夠從類加載器的角度來解決該問題,可參考博文——若是jar包衝突不可避免,如何實現jar包隔離,其思路值得借鑑。

2、有效避免

  從上一節的解決方案能夠發現,當出現第二類Jar包衝突,且衝突的Jar包又沒法排除時,問題變得至關棘手,這時候要處理該衝突問題就須要較大成本了,因此,最好的方式是在衝突發生以前能有效地規避之!就比如數據庫死鎖問題,死鎖避免和死鎖預防就顯得至關重要,如果等到真正發生死鎖了,常規的作法也只能是回滾並重啓部分事務,這就捉襟見肘了。那麼怎樣纔能有效地規避Jar包衝突呢?

2.1 良好的習慣:依賴管理

  對於第一類Jar包衝突問題,一般的作法是用**<excludes>排除不須要的版本,但這種作法帶來的問題是每次引入帶有傳遞性依賴的Jar包時,都須要一一進行排除,很是麻煩。maven爲此提供了集中管理依賴信息的機制,即依賴管理元素<dependencyManagement>**,對依賴Jar包進行統一版本管理,一勞永逸。一般的作法是,在parent模塊的pom文件中儘量地聲明全部相關依賴Jar包的版本,並在子pom中簡單引用該構件便可。

  來看個示例,當開發時肯定使用的httpclient版本爲4.5.1時,可在父pom中配置以下:

...
    <properties>
      <httpclient.version>4.5.1</httpclient.version>
    </properties>
    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.apache.httpcomponents</groupId>
          <artifactId>httpclient</artifactId>
          <version>${httpclient.version}</version>
        </dependency>
      </dependencies>
    </dependencyManagement>
...

  而後各個須要依賴該Jar包的子pom中配置以下依賴:

...
    <dependencies>
      <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
      </dependency>
    </dependencies>
...

2.2 衝突檢測插件

  對於第二類Jar包衝突問題,前面也提到過,其核心在於同名類出如今了多個不一樣的Jar包中,若是人工來排查該問題,則須要逐個點開每一個Jar包,而後相互對比看有沒同名的類,那得多麼浪費精力啊?!好在這種費時費力的體力活能交給程序去幹。maven-enforcer-plugin,這個強大的maven插件,配合extra-enforcer-rules工具,能自動掃描Jar包將衝突檢測並打印出來,汗顏的是,筆者工做以前竟然都沒聽過有這樣一個插件的存在,也許是沒遇到像工做中這樣的衝突問題,算是漲姿式了。其原理其實也比較簡單,經過掃描Jar包中的class,記錄每一個class對應的Jar包列表,若是有多個便是衝突了,故沒必要深究,咱們只須要關注如何用它便可。

  在最終須要打包運行的應用模塊pom中,引入maven-enforcer-plugin的依賴,在build階段便可發現問題,並解決它。好比對於具備parent pom的多模塊項目,須要將插件依賴聲明在應用模塊的pom中。這裏有童鞋可能會疑問,爲何不把插件依賴聲明在parent pom中呢?那樣依賴它的應用子模塊豈不是都能複用了?這裏之因此強調「打包運行的應用模塊pom」,是由於衝突檢測針對的是最終集成的應用,關注的是應用運行時是否會出現衝突問題,而每一個不一樣的應用模塊,各自依賴的Jar包集合是不一樣的,由此而產生的**<ignoreClasses>**列表也是有差別的,所以只能針對應用模塊pom分別引入該插件。

先看示例用法以下:

...
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>1.4.1</version>
  <executions>
    <execution>
      <id>enforce</id>
      <configuration>
        <rules>
          <dependencyConvergence/>
        </rules>
      </configuration>
      <goals>
        <goal>enforce</goal>
      </goals>
    </execution>
    <execution>
      <id>enforce-ban-duplicate-classes</id>
      <goals>
        <goal>enforce</goal>
      </goals>
      <configuration>
        <rules>
          <banDuplicateClasses>
            <ignoreClasses>
              <ignoreClass>javax.*</ignoreClass>
              <ignoreClass>org.junit.*</ignoreClass>
              <ignoreClass>net.sf.cglib.*</ignoreClass>
              <ignoreClass>org.apache.commons.logging.*</ignoreClass>
              <ignoreClass>org.springframework.remoting.rmi.RmiInvocationHandler</ignoreClass>
            </ignoreClasses>
            <findAllDuplicates>true</findAllDuplicates>
          </banDuplicateClasses>
        </rules>
        <fail>true</fail>
      </configuration>
    </execution>
  </executions>
  <dependencies>
    <dependency>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>extra-enforcer-rules</artifactId>
      <version>1.0-beta-6</version>
    </dependency>
  </dependencies>
</plugin>

  maven-enforcer-plugin是經過不少預約義的標準規則(standard rules)和用戶自定義規則,來約束maven的環境因素,如maven版本、JDK版本等等,它有不少好用的特性,具體可參見官網。而Extra Enforcer Rules則是MojoHaus項目下的針對maven-enforcer-plugin而開發的提供額外規則的插件,這其中就包含前面所提的重複類檢測功能,具體用法可參見官網,這裏就不詳細敘述了。

典型案例

第一類Jar包衝突

  這類Jar包衝突是最多見的也是相對比較好解決的,已經在3、衝突的表象這節中列舉了部分案例,這裏就不重複列舉了。

第二類Jar包衝突

Spring2.5.6與Spring3.x

  Spring2.5.6與Spring3.x,從單模塊拆分爲多模塊,Jar包名稱(artifactId)也從spring變爲spring-submoduleName,如 spring-context、spring-aop等等,而且也有少部分接口改動(Jar包升級的過程當中,這也是在所不免的)。因爲是不一樣的Jar包,經maven的傳遞依賴機制,就會常常性的存在這倆版本的Spring都在classpath中,從而引起潛在的衝突問題。

相關文章
相關標籤/搜索