依賴衝突是平常開發中常常碰到的過程,若是運氣好,並不會有什麼問題。恰恰小黑哥有點背,碰到好幾回生產問題,排查一整晚,最後發現倒是依賴衝突的引發的問題。java
沒碰到過這個問題同窗可能沒什麼感受,小黑哥舉兩個最近碰到例子,讓你們感覺一些。mysql
例子 1:spring
咱們公司有個古老的業務基礎包 A。B,C 業務依賴這個包。某個團隊拷貝 A 的部分代碼進行重構,類名與路徑徹底同樣,而後從新打包成 D 發佈。sql
一次業務改動,B 業務也引入了 D 包,測試環境運行的時候,一切 OK,可是在生產運行時,卻拋出 NoSuchMethodError
。數據庫
問題緣由在於 B 業務依賴 A,D。而 A,D 存在兩個同包同名類,運行的時候,具體加載誰,不一樣環境還真不同。apache
例子 2:maven
A 業務使用 Dubbo
進行 RPC
調用, Dubbo
須要依賴 javassist
。當前依賴關係爲:ide
A------->Dubbo------->javassist-3.18.1.GA
複製代碼
某次改動中引入另一個第三方開源包,其依賴 javassist-3.15.0-GA
。生產發佈的時候,將 javassist-3.15.0-GA
打包到應用中,因爲生產環節爲 JDK1.8,從而致使運行直接失敗。spring-boot
除了上述問題,依賴衝突還可能致使應用拋出 ClassNotFoundException
,NoClassDefFoundError
等錯誤。工具
拋出錯誤這種狀況還算好,還比較容易定位問題。怕就怕,不一樣版本同一個類內部邏輯不一樣,從而致使業務異常。這種問題,真的很讓人抓狂,讓人頭禿。
仔細分析依賴衝突,主要能夠分爲兩類:
下面咱們分析一下依賴衝突產生的緣由。
Maven
依賴分爲兩種狀況,直接依賴與間接依賴,這個比較好理解,你們直接看圖就好。
若是 A 應用間接依賴多個 C 應用,且版本都不同,Maven 將會經過仲裁機制選擇:
第一條原則,咱們下面再說。
第二條原則,以下圖:
A 間接依賴兩個版本 E,這種狀況下,因爲 A 到 E-1.0 路徑最短,因此 A 中將會使用 E-1.0。
若是路徑剛好同樣,那麼這種狀況下 Maven
只能根據 pom
中的順序,選擇最早聲明的,這也是個無奈的選擇。
Maven 項目能夠分爲三個階段:編譯階段,測試階段,運行階段了。經過 scope
屬性,咱們能夠決定依賴應用是否參與以上階段,也將會影響依賴傳遞。
Maven
提供 6 種 scope
:
compile
provided
runtime
test
system
import
compile
compile
是 Maven
默認屬性,將會使依賴包參與項目的編譯,測試,運行階段。固然,項目打包以後將會包含該依賴。
provided
provided
意味着依賴僅參與項目編譯,測試的階段。如有以下依賴關係:
A----->B----->C
複製代碼
C 的 scope
爲provided
,C 將會參與 B 的編譯,測試階段,可是 C 不會傳遞給 A。若是 A 運行過程須要 C,須要本身直接引入 C 依賴。典型如 Servlet API
,由於 Tomcat
等容器內部會提供。
runtime
runtime
表明依賴再也不參與項目編譯階段,只參與測試,運行階段。
若依賴不參與編譯階段,這種狀況 IDE 中是沒法導入相應的類的。若存在依賴類,編譯過程當中將會報錯。
典型的例子是 JDBC
驅動包,如 mysql
:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
<scope>runtime</scope>
</dependency>
複製代碼
知識點:這個好處在於,只能使用
JDBC
標準接口,這樣就不會與特定的數據庫綁定。後續若切換數據庫,只須要更換pom
,而後修改相應的參數便可。
test
test
僅參與測試階段的工做,典型的例子爲 junit
:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
複製代碼
system
system
與 provided
範圍一致,只不過 system
須要使用 systemPath 屬性指定本地路徑,而 provided
將會從 Maven
倉庫拉取。
import
import
比較特殊,不會參與以上階段運行。其只能在 dependencyManagement
下使用,且 type
須要爲 pom
。典型的例子爲 Spring-boot 依賴。
<dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
複製代碼
知識點:經過這種方式,解決單繼承問題,也能夠更好將依賴分類。
另外 Maven scope
將會影響依賴傳遞。
若是依賴關係爲: A--->B--->C,A 依賴 B,B 依賴 C。最左列表明 B 的
scope
屬性,第一行表明 C 的scope
屬性
如上所示,當 C 的 scope
爲 provided/test, C 只在 B 中起做用,不會經過間接依賴傳遞給 A。
當且僅當 B 的 scope
爲 compile
,且 C scope
爲 runtime
,A 將會間接依賴 C,且 scope
爲 runtime
。其餘狀況下,C 的 scope 將會與 B 的 scope 一致。
依賴衝突時,根據錯誤日誌,定位到衝突類,定位相應 jar
包,最後經過 excludes 排除相應的包。
另外能夠結合 IDEA Maven Helper 插件,主動檢查衝突依賴,提早排除。
經過插件,咱們能夠清晰看到衝突包,以及依賴路徑,還有相應的 Scope
。
除了排除依賴,咱們能夠經過合理的設置 scope
屬性,不讓依賴傳播下去。好比說,A 須要是使用 Spring-beans
包中某些類。若是其餘項目鐵定會使用 Spring,那麼咱們能夠將 A 中 Spring-beans
scope
設置爲 provided
,讓其餘項目本身選擇引入 Spring-beans
的版本。
這個適合公共基礎包,其餘包不要隨便使用
provided
,若使用必定要寫清楚,使用過程當中須要引入的依賴。
以上方法雖然治標,可是不治本。若是想依賴衝突不發生,咱們須要提早創建必定的規範,團隊一塊兒遵照,纔能有效避免該類問題。
dependencyManagement
統一管理基礎依賴,定義統一的版本,如經常使用中間包,工具包,日誌包。snapshot
替換成正式版本。雖然 snapshot
修改起來很方便,可是正由於這個特性,能夠被隨便修改。若是某次生產打包發佈不注意,就會引入。cmomon-lang3
是 common-lang
升級版, cmomon-lang3
包名爲 org.apache.commons.lang3,而 common-lang
包名爲 org.apache.commons.lang
若是咱們把 NPE
問題當作新手村普通怪物,那麼依賴衝突問題就是人馬這種精英怪。剛開始遇到,咱們會被虐的比較慘。只有咱們不斷升級,學習掌握技巧,而後才能能夠從容不迫解決。
ps:塞爾達中,大家第一次碰見人馬,打了幾回?小黑哥記得那天整整從晚上九點打到凌晨兩點,就是打不過啊~
最後用一張思惟導圖,總結文章內容。
這篇文章寫的很好,你們能夠看下。,從新看待Jar包衝突問題及解決方案
歡迎關注個人公衆號:程序通事,得到平常乾貨推送。若是您對個人專題內容感興趣,也能夠關注個人博客:studyidea.cn