Jar包衝突是老生常談的問題,幾乎每個Java程序猿都不可避免地遇到過,而且也都能想到一般的緣由通常是同一個Jar包因爲maven傳遞依賴等緣由被引進了多個不一樣的版本而致使,可採用依賴排除、依賴管理等常規方式來嘗試解決該問題,但這些方式真正能完全解決該衝突問題嗎?答案是否認的。筆者之因此將文章題目起爲「從新看待」,是由於以前對於Jar包衝突問題的理解僅僅停留在前面所說的那些,直到在工做中遇到的一系列Jar包衝突問題後,才發現並非那麼簡單,對該問題有了從新的認識,接下來本文將圍繞Jar包衝突的問題本質和相關的解決方案這兩個點進行闡述。html
Jar包衝突的本質是什麼?Google了半天也沒找到一個讓人滿意的完整定義。其實,咱們能夠從Jar包衝突產生的結果來總結,在這裏給出以下定義(此處若有不妥,歡迎拍磚~-~):java
Java應用程序因某種因素,加載不到正確的類而致使其行爲跟預期不一致。web
具體來講可分爲兩種狀況:1)應用程序依賴的同一個Jar包出現了多個不一樣版本,並選擇了錯誤的版本而致使JVM加載不到須要的類或加載了錯誤版本的類,爲了敘述的方便,筆者稱之爲第一類Jar包衝突問題;2)一樣的類(類的全限定名徹底同樣)出如今多個不一樣的依賴Jar包中,即該類有多個版本,並因爲Jar包加載的前後順序致使JVM加載了錯誤版本的類,稱之爲第二類Jar包問題。這兩種狀況所致使的結果實際上是同樣的,都會使應用程序加載不到正確的類,那其行爲天然會跟預期不一致了,如下對這兩種類型進行詳細分析。spring
隨着Jar包迭代升級,咱們所依賴的開源的或公司內部的Jar包工具都會存在若干不一樣的版本,而版本升級天然就避免不了類的方法簽名變動,甚至於類名的更替,而咱們當前的應用程序每每依賴特定版本的某個類 M ,因爲maven的傳遞依賴而致使同一個Jar包出現了多個版本,當maven的仲裁機制選擇了錯誤的版本時,而剛好類 M在該版本中被去掉了,或者方法簽名改了,致使應用程序因找不到所需的類 M或找不到類 M中的特定方法,就會出現第一類Jar衝突問題。可總結出該類衝突問題發生的如下三個必要條件:數據庫
一樣的類出如今了應用程序所依賴的兩個及以上的不一樣Jar包中,這會致使什麼問題呢?咱們知道,同一個類加載器對於同一個類只會加載一次(多個不一樣類加載器就另說了,這也是解決Jar包衝突的一個思路,後面會談到),那麼當一個類出如今了多個Jar包中,假設有 A 、 B 、 C 等,因爲Jar包依賴的路徑長短、聲明的前後順序或文件系統的文件加載順序等緣由,類加載器首先從Jar包 A 中加載了該類後,就不會加載其他Jar包中的這個類了,那麼問題來了:若是應用程序此時須要的是Jar包 B 中的類版本,而且該類在Jar包 A 和 B 中有差別(方法不一樣、成員不一樣等等),而JVM卻加載了Jar包 A 的中的類版本,與指望不一致,天然就會出現各類詭異的問題。apache
從上面的描述中,能夠發現出現不一樣Jar包的衝突問題有如下三個必要條件:bootstrap
當前maven大行其道,說到第一類Jar包衝突問題的產生緣由,就不得不提maven的依賴機制了。傳遞性依賴是Maven2.0引入的新特性,讓咱們只需關注直接依賴的Jar包,對於間接依賴的Jar包,Maven會經過解析從遠程倉庫獲取的依賴包的pom文件來隱式地將其引入,這爲咱們開發帶來了極大的便利,但與此同時,也帶來了常見的問題——版本衝突,即同一個Jar包出現了多個不一樣的版本,針對該問題Maven也有一套仲裁機制來決定最終選用哪一個版本,但Maven的選擇每每不必定是咱們所指望的,這也是產生Jar包衝突最多見的緣由之一。先來看下Maven的仲裁機制:tomcat
從maven的仲裁機制中能夠發現,除了第一條仲裁規則(這也是解決Jar包衝突的經常使用手段之一)外,後面的兩條原則,對於同一個Jar包不一樣版本的選擇,maven的選擇有點「一廂情願」了,也許這是maven研發團隊在總結了大量的項目依賴管理經驗後得出的兩條結論,又或者是發現根本找不到一種統一的方式來知足全部場景以後的無奈之舉,可能這對於多數場景是適用的,可是它不必定適合我——當前的應用,由於每一個應用都有其特殊性,該依賴哪一個版本,maven沒辦法幫你徹底搞定,若是你沒有規規矩矩地使用**<dependencyManagement>**來進行依賴管理,就註定了逃脫不了第一類Jar包衝突問題。app
對於第二類Jar包衝突問題,即多個不一樣的Jar包有類衝突,這相對於第一類問題就顯得更爲棘手。爲何這麼說呢?在這種狀況下,兩個不一樣的Jar包,假設爲 A、 B,它們的名稱互不相同,甚至可能徹底不沾邊,若是不是出現衝突問題,你可能都不會發現它們有共有的類!對於A、B這兩個Jar包,maven就顯得無能爲力了,由於maven只會爲你針對同一個Jar包的不一樣版本進行仲裁,而這倆是屬於不一樣的Jar包,超出了maven的依賴管理範疇。此時,當A、B都出如今應用程序的類路徑下時,就會存在潛在的衝突風險,即A、B的加載前後順序就決定着JVM最終選擇的類版本,若是選錯了,就會出現詭異的第二類衝突問題。eclipse
那麼Jar包的加載順序都由哪些因素決定的呢?具體以下:
Bootstrap Entries
中仍是User Entries
中呢,則須要仔細斟酌下咯。Jar包衝突可能會致使哪些問題?一般發生在編譯或運行時,主要分爲兩類問題:一類是比較直觀的也是最爲常見的錯誤是拋出各類運行時異常,還有一類就是比較隱晦的問題,它不會報錯,其表現形式是應用程序的行爲跟預期不一致,分條羅列以下:
CTRL+SHIFT+T
或者在idea中CTRL+N
就可發現該類存在於多個依賴Jar包中-verbose:class
或者-XX:+TraceClassLoading
,日誌裏會打印出每一個類的加載信息,如來自哪一個Jar包mvn dependency:tree -Dverbose -Dincludes=<groupId>:<artifactId>
查看是哪些地方引入的Jar包的這個版本從上一節的解決方案能夠發現,當出現第二類Jar包衝突,且衝突的Jar包又沒法排除時,問題變得至關棘手,這時候要處理該衝突問題就須要較大成本了,因此,最好的方式是在衝突發生以前能有效地規避之!就比如數據庫死鎖問題,死鎖避免和死鎖預防就顯得至關重要,如果等到真正發生死鎖了,常規的作法也只能是回滾並重啓部分事務,這就捉襟見肘了。那麼怎樣纔能有效地規避Jar包衝突呢?
對於第一類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> ...
對於第二類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包衝突是最多見的也是相對比較好解決的,已經在3、衝突的表象這節中列舉了部分案例,這裏就不重複列舉了。
Spring2.5.6與Spring3.x,從單模塊拆分爲多模塊,Jar包名稱(artifactId)也從spring變爲spring-submoduleName,如 spring-context、spring-aop等等,而且也有少部分接口改動(Jar包升級的過程當中,這也是在所不免的)。因爲是不一樣的Jar包,經maven的傳遞依賴機制,就會常常性的存在這倆版本的Spring都在classpath中,從而引起潛在的衝突問題。