在許多開發人員中,類加載器是Java語言的底層,并且經常被忽略。 在ZeroTurnaround上 ,我們的開發人員必須生活,呼吸,飲食,喝酒,并且幾乎與類加載器保持親密關系,才能生產JRebel技術,該技術在類加載器級別進行交互以提供實時運行時類重裝,從而避免了冗長的重建/重新包裝/重新部署周期。
以下是我們從類加載器中學到的一些知識,其中包括一些調試技巧,這些技巧將有望為您節省時間和將來的總服務臺。
一個類加載器只是一個普通的java對象
是的,這并不聰明,除了JVM中的系統類加載器之外,類加載器只是一個Java對象! 這是一個抽象類ClassLoader,可以由您創建的類實現。 這是API:
public abstract class ClassLoader {public Class loadClass(String name);protected Class defineClass(byte[] b);public URL getResource(String name);public Enumeration getResources(String name);public ClassLoader getParent()}
看起來很簡單,對吧? 讓我們逐個方法看一下。 中心方法是loadClass,它僅使用String類名,然后返回實際的Class對象。 如果您以前使用過類加載器,則此方法可能是最熟悉的方法,因為它是日常編碼中使用最多的方法。 defineClass是JVM中的最終方法,該方法從網絡上的文件或位置獲取字節數組,并產生相同的結果(即Class對象)。
類加載器還可以從類路徑中找到資源。 它的工作方式與loadClass方法類似。 有兩種方法,getResource和getResources,它們返回一個URL或URL的枚舉,這些URL或URL的枚舉指向資源,該資源表示傳遞給方法的名稱。
每個類加載器都有一個父級。 getParent返回與Java繼承無關的classloader父類,而是一個鏈表樣式的連接。 稍后我們將對此進行更深入的研究。
類加載器是惰性的,因此僅在運行時請求類時才加載類。 類是由調用該類的資源加載的,因此,在運行時,一個類可以由多個類加載器加載,具體取決于從何處引用它們,以及哪個類加載器加載了引用了這些類的類…哎呀,我cross地了! 讓我們看一些代碼。
public class A {public void doSmth() {B b = new B();b.doSmthElse();}}
在這里,我們有一個類A在其方法范圍內調用類B的構造函數。 在幕后這是正在發生的事情
A.class.getClassLoader().loadClass(“B”);
最初加載類A的類加載器被調用以加載類B。
類加載器是分層的,但是像孩子一樣,他們并不總是問父母
每個類加載器都有一個父類加載器。 當一個類加載器被要求提供一個類時,它通常會直接轉到父類加載器,首先調用loadClass,而后者又會詢問它的父類,依此類推。 如果要求具有相同父級的兩個類加載器加載同一類,則父級將只執行一次。 當兩個類加載器分別加載同一個類時,這將非常麻煩,因為這可能會導致問題,我們將在后面討論。
當設計JEE規范時,Web類加載器被設計為以相反的方式工作-很棒。 讓我們看一下下圖作為示例。
模塊WAR1有自己的類加載器,并且更喜歡自行加載類,而不是委托給其父級(由App1.ear定義的類加載器)。 這意味著不同的WAR模塊(例如WAR1和WAR2)無法看到彼此的類。 App1.ear模塊具有自己的類加載器,并且是WAR1和WAR2類加載器的父級。 當WAR1和WAR2類加載器需要在層次結構中委派請求時,即WAR類加載器范圍之外需要一個類時,它們將使用App1.ear類加載器。 實際上,WAR類會覆蓋同時存在的EAR類。 最后,EAR類加載器的父級是容器類加載器。 EAR類加載器會將請求委派給容器類加載器,但它的執行方式與WAR類加載器不同,因為EAR類加載器實際上更喜歡委托而不是本地類。 如您所見,這變得非常繁瑣,并且與普通的JSE類加載行為不同。
平面類路徑
我們討論了系統類加載器如何通過類路徑查找已請求的類。 該類路徑可能包含目錄或JAR文件,查找它們的順序實際上取決于您使用的JVM。 您在類路徑上可能需要該類的多個副本或版本,但是您將始終在類路徑上找到該類的第一個實例。 本質上,這只是資源列表,這就是為什么將其稱為扁平資源。 結果,在查找資源時,遍歷類路徑列表通常會比較慢。
當使用相同類路徑的應用程序想要使用類的不同版本時,可能會發生問題,讓我們以Hibernate為例。 當類路徑上存在兩個版本的Hibernate JAR時,一個版本不能比一個應用程序的版本路徑在另一個應用程序的類路徑上更高,這意味著兩個版本都必須使用相同的版本。 解決此問題的一種方法是使用所有必需的庫使應用程序(WAR)膨脹,以便它們使用其本地資源,但這會導致難以維護的大型應用程序。 歡迎來到JAR地獄! OSGi在此提供了一種解決方案,因為它允許對JAR文件或捆綁軟件進行版本控制,從而形成一種機制,允許連接到特定版本的JAR文件,從而避免了平坦的類路徑問題。
如何調試類加載錯誤?
NoClassDefFoundError / ClassNotFoundException / ClassNoDefFoundException?
因此,您遇到了上述錯誤/異常。 好吧,這個班級真的存在嗎? 不要在IDE中尋找麻煩,因為在那兒編譯類是必須的,因為它必須在那里,否則您將獲得編譯時異常。 這是一個運行時異常,因此在運行時我們要查找它說我們缺少的類……但是您從哪里開始呢? 考慮下面的代碼…
Arrays.toString((((URLClassLoader) Test.class.getClassLoader()).getURLs()));
此代碼返回Test正在使用的類加載器的類路徑上所有jar和目錄的數組列表。 現在,我們可以看到神秘類應該存在的JAR或位置實際上在類路徑上。 如果不存在,請添加! 如果確實存在,請檢查JAR /目錄,以確保您的類確實存在于該位置,并在缺少該類時添加它。 這是導致此錯誤情況的兩個典型問題。
NoSuchMethodError / NoSuchFieldError / AbstractMethodError / IllegalAccessError嗎?
現在變得越來越有趣了! 這些都是IncompatibleClassChangeError的所有子類。 我們知道類加載器已經找到了想要的類(按名稱),但是顯然它沒有找到正確的版本。
在這里,我們有一個稱為Test的類,它正在調用另一個類Util,但是BANG –我們遇到了異常! 讓我們看一下要調試的下一個代碼片段:
Test.class.getClassLoader().getResource(Util.class.getName().replace('.', '/') + ".class");
我們在類Test的類加載器上調用getResource。 這將向我們返回Util資源的URL。 請注意,我們已替換了“。” 帶有“ /”,并在字符串末尾添加“ .class”。 這會將我們正在尋找的類的包和類名(從類加載器的角度來看)更改為文件系統上的目錄結構和文件名-簡潔。 這將向我們顯示我們已加載的確切類,并且可以確保它是正確的版本。 我們可以在命令提示符下在類上使用javap -private來查看字節碼并檢查實際存在的方法和字段。 您可以輕松地查看該類的結構,并驗證是您還是瘋了的Java運行時! 相信我,在一個或另一個階段,您都會同時問這兩個問題,幾乎每次都是您!
LinkageError / ClassCastException / IllegalAccessError
如果兩個不同的類加載器加載同一個類,并且它們嘗試進行交互,則可能會發生這種情況。 是的,現在有點毛了。 這可能會導致問題,因為我們不知道它們是否將從同一位置加載類。 怎么會這樣 讓我們看下面的代碼,它們仍然在Test類中:
Factory.instance().sayHello();
該代碼看起來非常干凈和安全,尚不清楚如何從此行出現錯誤。 我們正在調用靜態工廠方法來獲取Test類的實例,并在其上調用方法。 讓我們看一下該支持圖像,以顯示引發異常的原因。
在這里,我們可以看到一個Web類加載器(加載了Test類)將優先使用本地類,因此,當它引用一個類時,將盡可能由Web類加載器加載。 到目前為止還算簡單。 Test類使用Factory類來獲取Util類的實例,這在Java中是很典型的做法,但是Factory類在WAR中并不存在,因為它是一個外部庫。 這是沒有問題的,因為Web類加載器可以委托給共享類加載器,后者可以看到Factory類。 請注意,共享類加載器現在正在加載它自己的Util類版本,因為當Factory實例化該類時,它使用了共享類加載器(如前面的第一個示例所示)。 Factory類將Util對象(由共享類加載器創建)返回給WAR,WAR然后嘗試使用該類,并將該類有效地強制轉換為同一類的潛在不同版本(Web類加載器可見的Util類) )。 繁榮!
我們可以在兩個地方(Factory.instance()方法和Test類)中運行與以前相同的代碼,以查看每個Util類從何處加載。
Test.class.getClassLoader().getResource(Util.class.getName().replace('.', '/') + ".class"));
希望這可以使您對類加載的世界有所了解,而不是不了解類加載器,現在可以帶著恐懼和不確定性來欣賞它! 感謝您的閱讀并將其制作到最后。 我們都希望您從ZeroTurnaround祝您圣誕快樂,新年快樂! 編碼愉快!
參考: 在JVM的底層– Java出現日歷博客中來自JCG合作伙伴 Simon Maple的類加載器 。
翻譯自: https://www.javacodegeeks.com/2012/12/under-the-jvm-hood-classloaders.html