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