㈠ MavenJar包沖突看看高手是怎麼解決的
接手了一套比較有年代感的系統,計劃把重構及遇到的問題寫成系列文章,老樹發新枝,重溫一些實戰技術,分享給大家。【重構02篇】:Maven項目Jar包管理機制、沖突解決。
知識背景Jar包沖突在軟體開發過程中是不可避免的,因此,如何快速定位沖突源,理解沖突導致的過程及底層原理,是每個程序員的必修課。也是提升工作效率、應對面試、在團隊中脫穎而出的機會。
實踐中能夠直觀感受到的Jar包沖突表現往往有這幾種:
程序拋出java.lang.ClassNotFoundException異常;
程序拋出java.lang.NoSuchMethodError異常;
程序拋出java.lang.NoClassDefFoundError異常;
程序拋出java.lang.LinkageError異常等;
這是能夠直觀呈現的,當然還有隱性的異常,比如程序執行結果與預期不符等。下面,我們就分析一下Maven項目中Jar包的處理機制及引起沖突的原因。
MavenJar包管理機制在Maven項目中,想要了解Jar沖突必須得先了解一下Maven是如何管理的Jar包的。這涉及到Maven的一些特性,比如依賴傳遞、最短路徑優先原則、最先聲明原則等。
依賴傳遞原則當在Maven項目中引入A的依賴,A的依賴通常又會引入B的jar包,B可能還會引入C的jar包。這樣,當你在pom.xml文件中添加了A的依賴,Maven會自動的幫你把所有相關的依賴都添加進來。
比如,在SpringBoot項中,當引入了spring-boot-starter-web:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>此時Maven的依賴結構可能是這樣的:
上面這種關系,我們就可以理解為依賴的傳遞性。即當一個依賴需要另外一個依賴支撐時,Maven會幫我們把相應的依賴依次添加到項目當中。
這樣的好處是,使用起來就非常方便,不用自己挨個去找依賴Jar包了。壞處是會引起Jar包沖突,我們後面會講到。
最短路徑優先原則依賴鏈路一:主要根據依賴的路徑長短來決定引入哪個依賴(兩個沖突的依賴)。
舉例說明:
依賴鏈路一:A->X->Y->Z(21.0)依賴鏈路二:B->Q->Z(20.0)項目中同時引入了A和B兩個依賴,它們間接都引入了Z依賴,但由於B的依賴鏈路比較短,因此最終生效的是Z(20.0)版本。這就是最短路徑優先原則。
此時如果Z的21.0版本和20.0版本區別較大,那麼就會發生Jar包沖突的表現。
最先聲明優先原則如果兩個依賴的路徑一樣,最短路徑優先原則是無法進行判斷的,此時需要使用最先聲明優先原則,也就是說,誰的聲明在前則優先選擇。
舉例說明:
依賴鏈路一:A->X->Z(21.0)依賴鏈路二:B->Q->Z(20.0)A和B最終都依賴Z,此時A的聲明(pom中引入的順序)優先於B,則針對沖突的Z會優先引入Z(21.0)。
如果Z(21.0)向下兼容Z(20.0),則不會出現Jar包沖突問題。但如果將B聲明放前面,則有可能會發生Jar包沖突。
Jar包沖突產生的原因上面講了Maven維護Jar包的三個原則,其實每個原則會發生什麼樣的Jar包沖突,已經大概了解了。這里再來一個綜合示例。
舉例說明:
依賴鏈路一:A->B->C->G21(guava21.0)依賴鏈路二:D->F->G20(guava20.0)假設項目中同時引入了A和D的依賴,按照依賴傳遞機制和默認依賴調節機制(第一:路徑最近者優先;第二:第一聲明優先),默認會引入G20版本的Jar包,而G21的Jar包不會被引用。
如果C中的方法使用了G21版本中的某個新方法(或類),由於Maven的處理,導致G21並未被引入。此時,程序在調用對應類時便會拋出ClassNotFoundException異常,調用對應方法時便會拋出NoSuchMethodError異常。
排查定位Jar包沖突在高版本的IDEA中已經自帶了Maven依賴管理插件,依次執行:打開pom.xml文件,在文件內右擊,選擇Maven,選擇ShowDependencies即可查看Maven的依賴層級結構:
執行之後展示的效果便是最開始的spring-boot-web那樣效果,在圖中可以清楚的看到都使用了哪些依賴,它們的層級,是否有沖突的jar包等。沖突部分會用紅色標出,同時標出Maven默認選擇了哪個版本。
如果你的IDEA版本中默認沒有Maven管理插件,也可安裝MavenHelper,通過這塊插件來幫你分析Jar包沖突。
安裝完插件,重啟之後,打開pom.xml文件,在文件下面的DependencyAnalyzer視圖中便可以看到Jar包沖突的結果分析:
此時,關於哪些Jar包沖突了,便一目瞭然。同時,可以右擊沖突的Jar包,執行」Exclude「進行排除,在pom.xml中便會自動添加排除jar包的屬性。
對於本地環境可以利用MavenHelper等插件來解決,但在預生產或生成環境中就沒那麼方便了。此時可以通過mvn命令來定位突出的細節。
執行如下mvn命令:
mvndependency:tree-Dverbose注意不要省略-Dverbose,要不然不會顯示被忽略的包。
執行結果如下:
通過這種形式,也可以清晰的看出哪些Jar包發生了沖突。
如何統一Jar包依賴像上面截圖所示,如果一個項目有N多個子項目構成,項目之間可能還有依賴關系,Jar包沖突不可避免,此時可採用父pom,統一對版本進行管理,一勞永逸。
通常做法,是在parent模塊的pom文件中盡可能地聲明所有相關依賴Jar包的版本,並在子pom中簡單引用(不再指定版本)該構件即可。
比如在父pom.xml中定義Lombok的版本:
<dependencyManagement><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.10</version></dependency></dependencies></dependencyManagement>在子mole中便可定義如下:
<dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies>通過這種方式,所有的子mole中都採用了統一的版本。
解決Jar包沖突的方法這里基於Maven項目介紹幾種場景下解決Jar沖突的方法:
Maven默認處理:採用此種方法,要牢記Maven依賴調節機制的基本原則,路徑最近者優先和第一聲明優先;
排除法:上面MavenHelper的實例中已經講到,可以將沖突的Jar包在pom.xml中通過exclude來進行排除;
版本鎖定法:如果項目中依賴同一Jar包的很多版本,一個個排除非常麻煩,此時可用版本鎖定法,即直接明確引入指定版本的依賴。根據前面介紹Maven處理Jar包基本原則,此種方式的優先順序最高。這種方法一般採用上面我們講到的如何統一Jar包依賴的方式。
Jar包沖突的本質上面講了Maven對項目中Jar包沖突的解決原則和實戰層面的解決方案,但並未涉及到Jar包沖突的本質。關於Jar包沖突的本質在《從Jar包沖突搞到類載入機制,就是這么霸氣》一文中已經進行詳細的講解了。這里再針對其中的幾個關鍵點進行概述一下。
Jar包沖突的本質:Java應用程序因某種因素,載入不到正確的類而導致其行為跟預期不一致。
具體分兩種情況:
情況一:項目依賴了同一Jar包的多個版本,並且選錯了版本;
情況二:同樣的類在不同的Jar包中出現,導致JVM載入了錯誤的類;
情況一,也是本文重點討論的場景,也就是說引入了多個Jar包版本,不同的Jar包版本有不同的類和方法。由於(不懂)Maven依賴樹的仲裁機制導致Maven載入了錯誤的Jar包,從而導致Jar包沖突;
情況二,同一類在不同的Jar包中出現(上篇文章中有詳細描述)。這種情況是由於JVM的同一個類載入器對於同一個類只會載入一次,現在載入一個類之後,同全限定名的類便不會進行載入,從而出現Jar包沖突的問題。
針對第二種情況,如果不是類沖突拋出了異常,你可能根本意識不到,所以就顯得更為棘手。這種情況就可以採用前文所述的通過分析不同類載入器的優先順序及載入路徑、文件系統的文件載入順序等進行調整來解決。
小結除了上述的方法,還很多小技巧來排查類沖突,比如通過IDE提供的搜索功能,直接搜索拋異常的類,看看是否存在多個,是否使用的是預期的版本等。這些技巧需要在實踐的過程中不斷的摸索和積累。
總之,無論項目多麼龐大,依賴多麼復雜,只要牢記導致沖突的原因,及解決沖突的幾個方式,細心分析,總會有跡可循的。看完這篇文章,實踐一下,你可能就會在團隊中脫穎而出,成為Jar包沖突終結者。
博主簡介:《SpringBoot技術內幕》技術圖書作者,酷愛鑽研技術,寫技術干貨文章。
公眾號:程序新視界