Jvm兩種機制:
1, 裝載具有合適名稱的類,---類裝載子系統
2, 負責執行一個負責執行包含在已裝載的類和接口中的指令,---運行引擎
每個jvm又包含方法區,棧區,堆區,程序計數器和本地方法
類加載器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態加載到 Java 虛擬機中並執行。類加載器從 JDK1.0 就出現了,最初是爲了滿足 Java Applet 的需要而開發出來的。JavaApplet 需要從遠程下載 Java 類文件到瀏覽器中並執行。現在類加載器在 Web 容器和 OSGi 中得到了廣泛的使用。一般來說,Java 應用的開發人員不需要直接同類加載器進行交互。Java 虛擬機默認的行爲就已經足夠滿足大多數情況的需求了。不過如果遇到了需要與類加載器進行交互的情況,而對類加載器的機制又不是很瞭解的話,就很容易花大量 的時間去調試ClassNotFoundException 和NoClassDefFoundError 等異常。本文將詳細介紹 Java 的類加載器,幫助讀者深刻理解 Java 語言中的這個重要概念。下面首先介紹一些相關的基本概念。
顧名思義,類加載器(classloader)用來加載 Java 類到 Java 虛擬機中。一般來說,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經過 Java 編譯器編譯之後就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成 java.lang.Class 類的一個實例。每個這樣的實例用來表示一個 Java 類。通過此實例的newInstance()方法就可以創建出該類的一個對象。實際的情況可能更加複雜,比如 Java 字節代碼可能是通過工具動態生成的,也可能是通過網絡下載的。
基本上所有的類加載器都是java.lang.ClassLoader 類的一個實例。下面詳細介紹這個 Java 類。
java.lang.ClassLoader 類介紹
java.lang.ClassLoader 類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,然後從這些字節代碼中定義出一個 Java 類,即java.lang.Class 類的一個實例。除此之外,ClassLoader 還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。不過本文只討論其加載類的功能。爲了完成加載類的這個職責,ClassLoader 提供了一系列的方法,比較重要的方法如1 所示。關於這些方法的細節會在下面進行介紹。
ClassLoader:
利用委託模式來搜索類和資源,每一個ClassLoader實例都有一個相關的父類加載器。
方法:
A,loadClass
1,調用findLoadedClass(String)檢查是否已經加載類
2,在父類加載器上調用loadClass方法,如果父類加載器爲null,則使用虛擬機的內置加載器。
3調用findClass(String)方法查找類。
B,defineClass
C,findSystemClass
D,resolveClass
E,findLoadedClass
Java2中classLoader的變動:
1, LoadClass的缺省實現
2, findClass
3, getSystemClassLoader
4, getParent
表 1.ClassLoader 中與加載類相關的方法
方法 |
說明 |
getParent() |
返回該類加載器的父類加載器。 |
loadClass(String name) |
加載名稱爲 name 的類,返回的結果是 java.lang.Class 類的實例。 |
findClass(String name) |
查找名稱爲 name 的類,返回的結果是 java.lang.Class 類的實例。 |
findLoadedClass(String name) |
查找名稱爲 name 的已經被加載過的類,返回的結果是 java.lang.Class 類的實例。 |
defineClass(String name, byte[] b, int off, int len) |
把字節數組 b 中的內容轉換成 Java 類,返回的結果是 java.lang.Class 類的實例。這個方法被聲明爲 final 的。 |
resolveClass(Class<?> c) |
鏈接指定的 Java 類。 |
對於給出的方法,表示類名稱的 name 參數的值是類的二進制名稱。需要注意的是內部類的表示,如 com.example.Sample$1 和com.example.Sample$Inner 等表示方式。這些方法會在下面介紹類加載器的工作機制時,做進一步的說明。下面介紹類加載器的樹狀組織結構。
Java 中的類加載器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:
- 引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。
- 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
- 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader() 來獲取它。
除了系統提供的類加載器以外,開發人員可以通過繼承 java.lang.ClassLoader 類的方式實現自己的類加載器,以滿足一些特殊的需求。
除了引導類加載器之外,所有的類加載器都有一個父類加載器。通過 1中給出的getParent() 方法可以得到。對於系統提供的類加載器來說,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對於開發人員編寫的類 加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因爲類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根 節點就是引導類加載器。1中給出了一個典型的類加載器樹狀組織結構示意圖,其中的箭頭指向的是父類加載器。
圖 1. 類加載器樹狀組織結構示意圖
1, 引導類加載器:java核心庫。
2, 擴展類加載器:擴展庫。
3, 系統類加載器:根據CLASSPATH來加載java類
清單 1. 演示類加載器的樹狀組織結構
public class ClassLoaderTree {
public static void main(String[] args) { ClassLoader loader = ClassLoaderTree.class.getClassLoader(); while (loader != null) { System.out.println(loader.toString()); loader = loader.getParent(); } } } |
每個 Java 類都維護着一個指向定義它的類加載器的引用,通過 getClassLoader() 方法就可以獲取到此引用。代碼 中通過遞歸調用getParent() 方法來輸出全部的父類加載器. 運行結果 所示。
清單 2. 演示類加載器的樹狀組織結構的運行結果
sun.misc.Launcher$AppClassLoader@9304b1 sun.misc.Launcher$ExtClassLoader@190d11 |
如 所示,第一個輸出的是ClassLoaderTree 類的類加載器,即系統類加載器。它是 sun.misc.Launcher$AppClassLoader 類的實例;第二個輸出的是擴展類加載器,是 sun.misc.Launcher$ExtClassLoader 類的實例。需要注意的是這裏並沒有輸出引導類加載器,這是由於有些 JDK 的實現對於父類加載器是引導類加載器的情況,getParent() 方法返回 null。
在瞭解了類加載器的樹狀組織結構之後,下面介紹類加載器的代理模式。
類加載器在嘗試自己去查找某個類的字節代碼並定義它時,會先代理給其父類加載器,由父類加載器先去嘗試加載這個類,依次類推。在介紹代理模式 背後的動機之前,首先需要說明一下 Java 虛擬機是如何判定兩個 Java 類是相同的。Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認爲兩個類是相同的。即便是同樣的字節代碼,被不同的類加 載器加載之後所得到的類,也是不同的。比如一個 Java 類com.example.Sample,編譯之後生成了字節代碼文件 Sample.class。兩個不同的類加載器ClassLoaderA 和 ClassLoaderB 分別讀取了這個Sample.class 文件,並定義出兩個 java.lang.Class 類的實例來表示這個類。這兩個實例是不相同的。對於 Java 虛擬機來說,它們是不同的類。試圖對這兩個類的對象進行相互賦值,會拋出運行時異常 ClassCastException。下面通過示例來具體說明。給出了 Java 類 com.example.Sample。
清單 3.com.example.Sample 類
package com.example;
public class Sample { private Sample instance;
public void setSample(Object instance) { this.instance = (Sample) instance; } } |
所示,com.example.Sample類的方法 setSample 接受一個java.lang.Object 類型的參數,並且會把該參數強制轉換成 com.example.Sample 類型。測試 Java 類是否相同的代碼所示。
清單 4. 測試 Java 類是否相同
public void testClassIdentity() { String classDataRootPath = "C:\\workspace\\Classloader\\classData"; FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); String className = "com.example.Sample"; try { Class<?> class1 = fscl1.loadClass(className); Object obj1 = class1.newInstance(); Class<?> class2 = fscl2.loadClass(className); Object obj2 = class2.newInstance(); Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); setSampleMethod.invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } } |
使用了類FileSystemClassLoader 的兩個不同實例來分別加載類 com.example.Sample,得到了兩個不同的java.lang.Class 的實例,接着通過 newInstance() 方法分別生成了兩個類的對象 obj1 和 obj2,最後通過 Java 的反射 API 在對象 obj1 上調用方法 setSample,試圖把對象 obj2 賦值給 obj1 內部的instance 對象。運行結果所示。
清單 5. 測試 Java 類是否相同的運行結果
java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26) at classloader.ClassIdentity.main(ClassIdentity.java:9) Caused by: java.lang.ClassCastException: com.example.Sample cannot be cast to com.example.Sample at com.example.Sample.setSample(Sample.java:7) ... 6 more |
給出的運行結果可以看到,運行時拋出了 java.lang.ClassCastException 異常。雖然兩個對象 obj1 和 obj2 的類的名字相同,但是這兩個類是由不同的類加載器實例來加載的,因此不被 Java 虛擬機認爲是相同的。
瞭解了這一點之後,就可以理解代理模式的設計動機了。代理模式是爲了保證 Java核心庫的類型安全。所有 Java 應用都至少需要引用 java.lang.Object類,也就是說在運行的時候,java.lang.Object這個類需要被加載到 Java 虛擬機中。如果這個加載過程由 Java 應用自己的類加載器來完成的話,很可能就存在多個版本的 java.lang.Object 類,而且這些類之間是不兼容的。通過代理模式,對於 Java 核心庫的類的加載工作由引導類加載器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。
不同的類加載器爲相同名稱的類創建了額外的名稱空間。相同名稱的類可以並存在 Java 虛擬機中,只需要用不同的類加載器來加載它們即可。不同類加載器加載的類之間是不兼容的,這就相當於在 Java 虛擬機內部創建了一個個相互隔離的 Java 類空間。這種技術在許多框架中都被用到,後面會詳細介紹。
下面具體介紹類加載器加載類的詳細過程。
在前面介紹類加載器的代理模式的時候,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類。這就意味着真正完成類的加載工作的類加載器和啓動這個加載過程的類加載器,有可能不是同一個。真正完成類的加載工作是通過調用 defineClass來實現的;而啓動類的加載過程是通過調用 loadClass來實現的。前者稱爲一個類的定義加載器(definingloader),後者稱爲初始加載器(initiating loader)。在 Java 虛擬機判斷兩個類是否相同的時候,使用的是類的定義加載器。也就是說,哪個類加載器啓動類的加載過程並不重要,重要的是最終定義這個類的加載器。兩種類加 載器的關聯之處在於:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer 引用了類com.example.Inner,則由類 com.example.Outer 的定義加載器負責啓動類com.example.Inner 的加載過程。
方法 loadClass()拋出的是java.lang.ClassNotFoundException異常;方法 defineClass()拋出的是java.lang.NoClassDefFoundError異常。
類加載器在成功加載某個類之後,會把得到的 java.lang.Class 類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,對於一個類加載器實例來說,相同全名的類只加載一次,即 loadClass 方法不會被重複調用。
下面討論另外一種類加載器:線程上下文類加載器。
線程上下文類加載器(contextclass loader)是從 JDK 1.2 開始引入的。類java.lang.Thread 中的方法 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 用來獲取和設置線程的上下文類加載器。如果沒有通過 setContextClassLoader(ClassLoader cl) 方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統類加載器。在線程中運行的代碼可以通過此類加載器來加載類和資源。
前面提到的類加載器的代理模式並不能解決 Java 應用開發中會遇到的類加載器的全部問題。Java 提供了很多服務提供者接口(ServiceProvider Interface,SPI),允許第三方爲這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers 包中。這些 SPI 的實現代碼很可能是作爲 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXPSPI 的 ApacheXerces 所包含的 jar 包。SPI 接口中的代碼經常需要加載具體的實現類。如 JAXP 中的javax.xml.parsers.DocumentBuilderFactory 類中的newInstance() 方法用來生成一個新的 DocumentBuilderFactory 的實例。這裏的實例的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在於,SPI 的接口是 Java核心庫的一部分,是由引導類加載器來加載的;SPI實現的 Java 類一般是由系統類加載器來加載的。引導類加載器是無法找到 SPI的實現類的,因爲它只加載 Java 的核心庫。它也不能代理給系統類加載器,因爲它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。
線程上下文類加載器正好解決了這個問題。如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現的類。線程上下文類加載器在很多 SPI 的實現中都會用到。
下面介紹另外一種加載類的方法:Class.forName。
Class.forName 是一個靜態方法,同樣可以用來加載類。該方法有兩種形式:Class.forName(String name, boolean initialize,ClassLoader loader) 和 Class.forName(String className)。第一種形式的參數 name 表示的是類的全名;initialize表示是否初始化類;loader 表示加載時使用的類加載器。第二種形式則相當於設置了參數 initialize 的值爲 true,loader 的值爲當前類的類加載器。Class.forName的一個很常見的用法是在加載數據庫驅動的時候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() 用來加載 Apache Derby 數據庫的驅動。
在介紹完類加載器相關的基本概念之後,下面介紹如何開發自己的類加載器。
定製ClassLoader步驟:
調用findLoadedClass來查看是否存在已經裝入的類
如果沒有,那麼採用特殊的神奇方式來獲取原始字節。
如果已有原始字節,調用defineClass將他們轉換成class對象。
如果沒有原始字節,然後調用findSystemClass查案是否從本地文件系統獲取類。
如果resolve參數是true,那麼調用resolveClass解析Class對象。
如果還沒有類,返回ClassNotFoundException.
否則,將類返回給調用程序。
雖然在絕大多數情況下,系統默認提供的類加載器實現已經可以滿足需求。但是在某些情況下,您還是需要爲應用開發出自己的類加載器。比如您的應 用通過網絡來傳輸 Java 類的字節代碼,爲了保證安全性,這些字節代碼經過了加密處理。這個時候您就需要自己的類加載器來從某個網絡地址上讀取加密後的字節代碼,接着進行解密和驗 證,最後定義出要在 Java 虛擬機中運行的類來。下面將通過兩個具體的實例來說明類加載器的開發。
第一個類加載器用來加載存儲在文件系統上的 Java 字節代碼。完整的實現所示。
清單 6. 文件系統類加載器
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; }
protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } }
private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; }
private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } } |
所示,類FileSystemClassLoader 繼承自類 java.lang.ClassLoader。列出的java.lang.ClassLoader 類的常用方法中,一般來說,自己開發的類加載器只需要覆寫 findClass(String name)方法即可。java.lang.ClassLoader 類的方法loadClass() 封裝了前面提到的代理模式的實現。該方法會首先調用 findLoadedClass() 方法來檢查該類是否已經被加載過;如果沒有加載過的話,會調用父類加載器的 loadClass() 方法來嘗試加載該類;如果父類加載器無法加載該類的話,就調用 findClass() 方法來查找該類。因此,爲了保證類加載器都正確實現代理模式,在開發自己的類加載器時,最好不要覆寫 loadClass() 方法,而是覆寫findClass() 方法。
類FileSystemClassLoader 的 findClass() 方法首先根據類的全名在硬盤上查找類的字節代碼文件(.class 文件),然後讀取該文件內容,最後通過 defineClass() 方法來把這些字節代碼轉換成java.lang.Class 類的實例。
下面將通過一個網絡類加載器來說明如何通過類加載器來實現組件的動態更新。即基本的場景是:Java 字節代碼(.class)文件存放在服務器上,客戶端通過網絡的方式獲取字節代碼並執行。當有版本更新的時候,只需要替換掉服務器上保存的文件即可。通過 類加載器可以比較簡單的實現這種需求。
類NetworkClassLoader 負責通過網絡下載 Java 類字節代碼並定義出 Java 類。它的實現與 FileSystemClassLoader 類似。在通過NetworkClassLoader 加載了某個版本的類之後,一般有兩種做法來使用它。第一種做法是使用 Java 反射 API。另外一種做法是使用接口。需要注意的是,並不能直接在客戶端代碼中引用從服務器上下載的類,因爲客戶端代碼的類加載器找不到這些類。使用 Java 反射 API 可以直接調用 Java 類的方法。而使用接口的做法則是把接口的類放在客戶端中,從服務器上加載實現此接口的不同版本的類。在客戶端通過相同的接口來使用這些實現類。
在介紹完如何開發自己的類加載器之後,下面說明類加載器和 Web 容器的關係。
類加載器與 Web 容器
對於運行在 JavaEE™ 容器中的 Web 應用來說,類加載器的實現方式與一般的 Java 應用有所不同。不同的 Web 容器的實現方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不同的是它是首先嚐試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順 序是相反的。這是 JavaServlet 規範中的推薦做法,其目的是使得 Web 應用自己的類的優先級高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找範圍之內的。這也是爲了保證 Java 核心庫的類型安全。
絕大多數情況下,Web 應用的開發人員不需要考慮與類加載器相關的細節。下面給出幾條簡單的原則:
- 每個 Web 應用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes 和 WEB-INF/lib 目錄下面。
- 多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面。
- 當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。
在介紹完類加載器與 Web 容器的關係之後,下面介紹它與 OSGi 的關係。
類加載器與 OSGi
OSGi™ 是 Java 上的動態模塊系統。它爲開發人員提供了面向服務和基於組件的運行環境,並提供標準的方式用來管理軟件的生命週期。OSGi 已經被實現和部署在很多產品上,在開源社區也得到了廣泛的支持。Eclipse 就是基於 OSGi 技術來構建的。
OSGi 中的每個模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導入(import)的其它模塊的 Java 包和類(通過 Import-Package),也可以聲明導出(export)自己的包和類,供其它模塊使用(通過 Export-Package)。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實現的。OSGi 中的每個模塊都有對應的一個類加載器。它負責加載模塊自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java 開頭的包和類),它會代理給父類加載器(通常是啓動類加載器)來完成。當它需要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設置系統屬性 org.osgi.framework.bootdelegation 的值即可。
假設有兩個模塊bundleA 和 bundleB,它們都有自己對應的類加載器classLoaderA 和 classLoaderB。在 bundleA 中包含類 com.bundleA.Sample,並且該類被聲明爲導出的,也就是說可以被其它模塊所使用的。bundleB 聲明瞭導入bundleA 提供的類 com.bundleA.Sample,幷包含一個類com.bundleB.NewSample 繼承自 com.bundleA.Sample。在 bundleB 啓動的時候,其類加載器 classLoaderB 需要加載類com.bundleB.NewSample,進而需要加載類 com.bundleA.Sample。由於bundleB 聲明瞭類 com.bundleA.Sample 是導入的,classLoaderB把加載類 com.bundleA.Sample 的工作代理給導出該類的bundleA 的類加載器 classLoaderA。classLoaderA 在其模塊內部查找類 com.bundleA.Sample 並定義它,所得到的類com.bundleA.Sample 實例就可以被所有聲明導入了此類的模塊使用。對於以 java 開頭的類,都是由父類加載器來加載的。如果聲明瞭系統屬性 org.osgi.framework.bootdelegation=com.example.core.*,那麼對於包 com.example.core 中的類,都是由父類加載器來完成的。
OSGi 模塊的這種類加載器結構,使得一個類的不同版本可以共存在 Java 虛擬機中,帶來了很大的靈活性。不過它的這種不同,也會給開發人員帶來一些麻煩,尤其當模塊需要使用第三方提供的庫的時候。下面提供幾條比較好的建議:
- 如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中,在 Bundle-ClassPath 中指明即可。
- 如果一個類庫被多個模塊共用,可以爲這個類庫單獨的創建一個模塊,把其它模塊需要用到的 Java 包聲明爲導出的。其它模塊聲明導入這些類。
- 如果類庫提供了 SPI 接口,並且利用線程上下文類加載器來加載 SPI 實現的 Java 類,有可能會找不到 Java 類。如果出現了 NoClassDefFoundError 異常,首先檢查當前線程的上下文類加載器是否正確。通過 Thread.currentThread().getContextClassLoader() 就可以得到該類加載器。該類加載器應該是該模塊對應的類加載器。如果不是的話,可以首先通過 class.getClassLoader() 來得到模塊對應的類加載器,再通過 Thread.currentThread().setContextClassLoader() 來設置當前線程的上下文類加載器。
類加載器是 Java 語言的一個創新。它使得動態安裝和更新軟件組件成爲可能。本文詳細介紹了類加載器的相關話題,包括基本概念、代理模式、線程上下文類加載器、與 Web 容器和 OSGi 的關係等。開發人員在遇到 ClassNotFoundException 和NoClassDefFoundError 等異常的時候,應該檢查拋出異常的類的類加載器和當前線程的上下文類加載器,從中可以發現問題的所在。在開發自己的類加載器的時候,需要注意與已有的類加載器組織結構的協調。