Jar Hell变得轻松–用jHades揭秘classpath
Java開發(fā)人員將不得不面對(duì)的最困難的問題是類路徑錯(cuò)誤: ClassNotFoundException , NoClassDefFoundError ,Jar Hell, Xerces Hell和company。
在本文中,我們將探究這些問題的根本原因,并了解最小的工具( JHades )如何幫助快速解決這些問題。 我們將看到為什么Maven無法(始終)防止類路徑重復(fù),并且:
- 處理地獄的唯一方法
- 裝載機(jī)
- 類加載器鏈
- 類加載器的優(yōu)先級(jí):父優(yōu)先與父末
- 調(diào)試服務(wù)器啟動(dòng)問題
- 用jHades理解Jar Hell
- 避免類路徑問題的簡(jiǎn)單策略
- 類路徑在Java 9中得到修復(fù)嗎?
處理地獄的唯一方法
類路徑問題的調(diào)試可能很耗時(shí),并且往往在最壞的時(shí)間和地點(diǎn)發(fā)生:發(fā)布之前,通常在開發(fā)團(tuán)隊(duì)幾乎沒有訪問權(quán)限的環(huán)境中。
它們也可能發(fā)生在IDE級(jí)別,并成為生產(chǎn)力降低的根源。 我們的開發(fā)人員往往會(huì)及早發(fā)現(xiàn)這些問題,這是通常的回答:
讓我們嘗試為我們節(jié)省一些時(shí)間,并深入探討這一點(diǎn)。 這些類型的問題很難通過反復(fù)試驗(yàn)來解決。 解決這些問題的唯一真正方法是真正了解正在發(fā)生的事情 ,但是從哪里開始呢?
事實(shí)證明,Jar Hell問題比其看起來要簡(jiǎn)單,并且僅需幾個(gè)概念即可解決它們。 最后,造成Jar Hell問題的常見根本原因是:
- 一個(gè)罐子不見了
- 一個(gè)罐子太多了
- 一個(gè)班級(jí)在什么地方不可見
但是,如果這么簡(jiǎn)單,那么為什么類路徑問題很難調(diào)試?
Jar Hell堆棧跟蹤不完整
原因之一是類路徑問題的堆棧跟蹤缺少許多信息來解決問題。 以以下堆棧跟蹤為例:
java.lang.IncompatibleClassChangeError: Class org.jhades.SomeServiceImpl does not implement the requested interface org.jhades.SomeService org.jhades.TestServlet.doGet(TestServlet.java:19)它說一個(gè)類沒有實(shí)現(xiàn)某個(gè)接口。 但是,如果我們查看類源代碼:
public class SomeServiceImpl implements SomeService { @Overridepublic void doSomething() {System.out.println( "Call successful!" );} }好了,該類顯然實(shí)現(xiàn)了缺少的接口! 那么發(fā)生了什么呢? 問題在于堆棧跟蹤缺少很多信息 ,這些信息對(duì)于理解該問題至關(guān)重要。
堆棧跟蹤可能應(yīng)該包含這樣的錯(cuò)誤消息(我們將了解這是什么意思):
類SomeServiceImpl類加載器/路徑/到/ Tomcat的/ lib中不實(shí)現(xiàn)接口SomeService從類加載器加載的Tomcat - Web應(yīng)用程序- /路徑/到/ Tomcat的/ web應(yīng)用/測(cè)試
這至少是從哪里開始的指示:
- 剛學(xué)習(xí)Java的人至少會(huì)知道,對(duì)于了解正在發(fā)生的事情,必不可少的是類加載器這一概念。
- 很明顯,涉及的一個(gè)類不是從WAR加載的,而是從服務(wù)器的某個(gè)目錄( SomeServiceImpl )加載的。
什么是類加載器?
首先,類加載器只是Java類,更確切地說是運(yùn)行時(shí)類的實(shí)例。 它不是 JVM不可訪問的內(nèi)部組件,例如垃圾收集器。
以Tomcat的WebAppClassLoader為例,這里是javadoc 。 如您所見,它只是一個(gè)普通的Java類,如果需要,我們甚至可以編寫我們自己的類加載器。
ClassLoader都可以用作類加載器。 類加載器的主要職責(zé)是知道類文件的位置,然后根據(jù)JVM的要求加載類。
一切都鏈接到類加載器
JVM中的每個(gè)對(duì)象都通過getClass()鏈接到其類,而每個(gè)類都通過getClassLoader()鏈接到類加載器。 這意味著:
JVM中的每個(gè)對(duì)象都鏈接到一個(gè)類加載器!
讓我們看看如何使用此事實(shí)對(duì)類路徑錯(cuò)誤方案進(jìn)行故障排除。
如何查找類文件的實(shí)際位置
我們來看一個(gè)對(duì)象,看看它的類文件在文件系統(tǒng)中的位置:
System.out.println(service.getClass() .getClassLoader().getResource("org/jhades/SomeServiceImpl.class")); 這是類文件的完整路徑: jar:file:/Users/user1/.m2/repository/org/jhades/jar-2/1.0-SNAPSHOT/jar-2-1.0-SNAPSHOT.jar!/org/jhades/SomeServiceImpl.class
jar:file:/Users/user1/.m2/repository/org/jhades/jar-2/1.0-SNAPSHOT/jar-2-1.0-SNAPSHOT.jar!/org/jhades/SomeServiceImpl.class
如我們所見,類加載器只是一個(gè)運(yùn)行時(shí)組件,它知道文件系統(tǒng)中查找類文件的位置以及如何加載它們。
但是,如果類加載器找不到給定的類,會(huì)發(fā)生什么?
類加載器鏈
缺省情況下,在JVM中,如果類加載器找不到類,則它將要求其父類加載器提供相同的類,依此類推。
這一直持續(xù)到JVM引導(dǎo)類加載器為止(稍后會(huì)對(duì)此進(jìn)行更多介紹)。 這個(gè)類加載器鏈?zhǔn)穷惣虞d器委托鏈 。
類加載器的優(yōu)先級(jí):父優(yōu)先與父末
一些類加載器將請(qǐng)求立即委派給父類加載器,而無需先在其自己的已知目錄集中搜索類文件。 據(jù)說在此模式下運(yùn)行的類加載器處于“ 父優(yōu)先”模式。
如果類加載器首先在本地查找類,并且僅在查詢父類(如果找不到該類)之后才查找,則該類加載器被稱為在“上一父代”模式下工作。
所有應(yīng)用程序都有類加載器鏈嗎?
甚至最簡(jiǎn)單的Hello World主方法也具有3個(gè)類加載器:
- 應(yīng)用程序類加載器,負(fù)責(zé)加載應(yīng)用程序類(父級(jí)優(yōu)先)
- 擴(kuò)展類加載器,它從$JAVA_HOME/jre/lib/ext (先是父級(jí))加載jar
- Bootstrap類加載器,用于加載JDK附帶的任何類,例如java.lang.String (無父類加載器)
WAR應(yīng)用程序的類加載器鏈?zhǔn)鞘裁礃拥?#xff1f;
對(duì)于Tomcat或Websphere之類的應(yīng)用程序服務(wù)器,類加載器鏈的配置與簡(jiǎn)單的Hello World主方法程序不同。 以Tomcat類加載器鏈為例:
在這里,我們希望每個(gè)WAR都在WebAppClassLoader運(yùn)行,該WebAppClassLoader以父級(jí)末尾模式工作(也可以將其設(shè)置為父級(jí)末尾)。 通用類加載器加載在服務(wù)器級(jí)別安裝的庫(kù)。
Servlet規(guī)范對(duì)類加載有何看法?
Servlet容器規(guī)范僅定義了類加載器鏈行為的一小部分:
- WAR應(yīng)用程序在其自己的應(yīng)用程序類加載器上運(yùn)行,可以與其他應(yīng)用程序共享或不與其他應(yīng)用程序共享
- WEB-INF/classes的文件優(yōu)先于其他所有文件
在那之后,任何人都可以猜測(cè)! 其余的完全開放給容器提供商解釋。
為什么在供應(yīng)商之間沒有通用的類加載方法?
通常,默認(rèn)情況下,通常將諸如Tomcat或Jetty之類的開源容器配置為先在WAR中查找類,然后才在服務(wù)器類加載器中搜索。
這使應(yīng)用程序可以使用自己的庫(kù)版本來覆蓋服務(wù)器上可用的庫(kù)。
大型鐵服務(wù)器呢?
諸如Websphere之類的商業(yè)產(chǎn)品將嘗試“出售”自己的服務(wù)器提供的庫(kù),這些庫(kù)默認(rèn)情況下優(yōu)先于WAR上安裝的庫(kù)。
假設(shè)您購(gòu)買了該服務(wù)器,并且還希望使用它提供的JEE庫(kù)和版本,則通常不會(huì)這樣做。
這給部署到某些商業(yè)產(chǎn)品帶來了極大的麻煩,因?yàn)樗鼈兊男袨榉绞讲煌陂_發(fā)人員用來在其工作站中運(yùn)行應(yīng)用程序的Tomcat或Jetty。 我們將在此解決方案上看到更多。
常見問題:重復(fù)的類版本
目前,您可能有一個(gè)很大的問題:
如果WAR中有兩個(gè)罐子包含完全相同的類怎么辦?
答案是行為是不確定的, 只有在運(yùn)行時(shí)才會(huì)選擇兩個(gè)類之一 。 選擇哪一個(gè)取決于類加載器的內(nèi)部實(shí)現(xiàn),無法預(yù)先知道。
但是幸運(yùn)的是,如今大多數(shù)項(xiàng)目都使用Maven,Maven通過確保僅將給定jar的一個(gè)版本添加到WAR中來解決此問題。
因此,Maven項(xiàng)目可以不受這種特定類型的Jar Hell的影響,對(duì)嗎?
為什么Maven不能防止類路徑重復(fù)
不幸的是,Maven無法在所有Jar Hell情況下提供幫助。 實(shí)際上,許多不使用某些質(zhì)量控制插件的Maven項(xiàng)目在類路徑上都可以有數(shù)百個(gè)重復(fù)的類文件(我看到中繼有500多個(gè)重復(fù)項(xiàng))。 這有幾個(gè)原因:
- 圖書館出版商有時(shí)會(huì)更改罐子的工件名稱:發(fā)生這種情況是由于品牌重塑或其他原因。 以JAXB jar為例。 Maven不可能將這些工件識(shí)別為同一罐子!
- 某些jar具有或不具有依賴關(guān)系而發(fā)布:一些庫(kù)提供程序提供jar的“具有依賴關(guān)系”版本,其中包括其他jar。 如果兩個(gè)版本都具有傳遞依賴,則最終將導(dǎo)致重復(fù)。
- 有些類在jar之間復(fù)制:有些庫(kù)創(chuàng)建者在遇到某個(gè)類的需要時(shí),只會(huì)從另一個(gè)項(xiàng)目中獲取它,然后將其復(fù)制到新的jar中而不更改包名。
所有的班級(jí)文件重復(fù)都是危險(xiǎn)的嗎?
如果重復(fù)的類文件存在于同一個(gè)類加載器中,并且兩個(gè)重復(fù)的類文件完全相同,那么首先選擇哪個(gè)是無關(guān)緊要的–這種情況并不危險(xiǎn)。
如果兩個(gè)類文件都在同一個(gè)類加載器中,并且它們不相同,則無法在運(yùn)行時(shí)選擇一個(gè),這是有問題的,并且在部署到不同環(huán)境時(shí)會(huì)表現(xiàn)出來。
如果類文件位于兩個(gè)不同的類加載器中,則永遠(yuǎn)不會(huì)將它們視為相同(請(qǐng)參見后面的類標(biāo)識(shí)危機(jī)部分)。
如何避免WAR類路徑重復(fù)?
例如,可以通過使用Maven Enforcer插件來避免此問題,并啟用“ 禁止重復(fù)類”的額外規(guī)則。
您也可以使用JHades WAR重復(fù)類報(bào)告快速檢查您的WAR是否干凈。 該工具可以過濾“無害”重復(fù)項(xiàng)(相同的類文件大小)。
但是,即使是干凈的WAR也會(huì)存在部署問題:類丟失,從服務(wù)器而不是WAR中獲取的類以及版本錯(cuò)誤的類,類強(qiáng)制轉(zhuǎn)換異常等。
使用JHades調(diào)試類路徑
類路徑問題通常在應(yīng)用服務(wù)器啟動(dòng)時(shí)出現(xiàn),這是一個(gè)特別糟糕的時(shí)刻,尤其是在部署到訪問受限的環(huán)境中時(shí)。
JHades是幫助處理Jar Hell的工具(免責(zé)聲明:我寫的)。 它是一個(gè)單一的Jar,除了JDK7本身之外,沒有任何依賴性。 這是一個(gè)如何使用它的示例:
new JHades().printClassLoaders().printClasspath().overlappingJarsReport().multipleClassVersionsReport().findClassByName("org.jhades.SomeServiceImpl")這會(huì)將類加載器鏈,罐子,重復(fù)類等打印到屏幕上。
調(diào)試服務(wù)器啟動(dòng)問題
在服務(wù)器無法正常啟動(dòng)的情況下,JHades可以很好地工作。 提供了一個(gè)servlet偵聽器,即使在應(yīng)用程序的任何其他組件開始運(yùn)行之前,該偵聽器也可以打印類路徑調(diào)試信息。
ClassCastException和類身份危機(jī)
對(duì)Jar Hell進(jìn)行故障排除時(shí),請(qǐng)注意ClassCastExceptions 。 在JVM中,不僅通過完全限定的類名來標(biāo)識(shí)類,而且還通過其類加載器來標(biāo)識(shí)該類。
這是違反直覺的,但事后看來是有道理的:我們可以使用相同的包和名稱創(chuàng)建兩個(gè)不同的類,將它們放入兩個(gè)jar中,然后放入兩個(gè)不同的類加載器中。 可以說一個(gè)擴(kuò)展了ArrayList ,另一個(gè)是Map 。
因此,這些類是完全不同的(盡管名稱相同),并且不能相互轉(zhuǎn)換! 運(yùn)行時(shí)將拋出CCE以防止發(fā)生這種潛在的錯(cuò)誤情況,因?yàn)闊o法保證這些類是可強(qiáng)制轉(zhuǎn)換的。
將類加載器添加到類標(biāo)識(shí)符是Java早期發(fā)生的類身份危機(jī)的結(jié)果。
避免類路徑問題的策略
說起來容易做起來難,但是避免與類路徑相關(guān)的部署問題的最佳方法是在“上一步”模式下運(yùn)行生產(chǎn)服務(wù)器。
這樣,WAR的類版本優(yōu)先于服務(wù)器上的類版本,并且在生產(chǎn)環(huán)境和開發(fā)人員工作站中使用了相同的類,這些工作站可能正在使用Tomcat,Jetty或其他開源的Parent Last服務(wù)器。
在某些服務(wù)器(例如Websphere)中,這還不夠,并且您還必須在清單文件中提供特殊屬性以顯式關(guān)閉某些庫(kù),例如JAX-WS。
修復(fù)Java 9中的類路徑
在Java 9中,類路徑已完全通過新的Jigsaw模塊化系統(tǒng)進(jìn)行了改進(jìn)。 在Java 9中,可以將jar聲明為模塊,它將在其自己的隔離類加載器中運(yùn)行,該類加載器以O(shè)SGI方式從其他類似的模塊類加載器讀取類文件。
如果需要,這將允許同一版本的Jar的多個(gè)版本共存。
結(jié)論
最后,Jar Hell問題并不是像最初看起來那樣低級(jí)或難以解決。 都是關(guān)于zip文件(jar)在某些目錄中存在/不存在,如何查找這些目錄以及如何在訪問受限的環(huán)境中調(diào)試類路徑。
通過了解一組有限的概念(例如類加載器,類加載器鏈和父級(jí)/父級(jí)后代模式),可以有效地解決這些問題。
外部鏈接
這份演講“您真的從ZeroTurnaround的Jevgeni Kabanov( JRebel公司) 獲得類加載器”是有關(guān)Jar Hell以及與類路徑相關(guān)的不同類型異常的重要資源。
翻譯自: https://www.javacodegeeks.com/2014/10/jar-hell-made-easy-demystifying-the-classpath-with-jhades.html
總結(jié)
以上是生活随笔為你收集整理的Jar Hell变得轻松–用jHades揭秘classpath的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 食品配方备案查询(食品配方备案)
- 下一篇: JDK8 lambda的会话指南–术语表