來自https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
類加載器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態加載到 Java 虛擬機中并執行。類加載器從 JDK 1.0 就出現了,最初是為了滿足 Java Applet 的需要而開發出來的。Java Applet 需要從遠程下載 Java 類文件到瀏覽器中并執行。現在類加載器在 Web 容器和 OSGi 中得到了廣泛的使用。一般來說,Java 應用的開發人員不需要直接同類加載器進行交互。Java 虛擬機默認的行為就已經足夠滿足大多數情況的需求了。不過如果遇到了需要與類加載器進行交互的情況,而對類加載器的機制又不是很了解的話,就很容易花大量的時間去調試?`ClassNotFoundException`和?`NoClassDefFoundError`等異常。本文將詳細介紹 Java 的類加載器,幫助讀者深刻理解 Java 語言中的這個重要概念。下面首先介紹一些相關的基本概念。
## 類加載器基本概念
顧名思義,類加載器(class loader)用來加載 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](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#minor1.1)所示。關于這些方法的細節會在下面進行介紹。
##### 表 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 類。 |
對于?[表 1](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#minor1.1)中給出的方法,表示類名稱的?`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](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#minor1.1)中給出的?`getParent()`方法可以得到。對于系統提供的類加載器來說,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對于開發人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因為類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。[圖 1](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#fig1)中給出了一個典型的類加載器樹狀組織結構示意圖,其中的箭頭指向的是父類加載器。
##### 圖 1\. 類加載器樹狀組織結構示意圖

[代碼清單 1](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code1)演示了類加載器的樹狀組織結構。
##### 清單 1\. 演示類加載器的樹狀組織結構
|
1
2
3
4
5
6
7
8
9
10
|
`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()`方法就可以獲取到此引用。[代碼清單 1](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code1)中通過遞歸調用?`getParent()`方法來輸出全部的父類加載器。[代碼清單 1](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code1)的運行結果如?[代碼清單 2](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code2)所示。
##### 清單 2\. 演示類加載器的樹狀組織結構的運行結果
|
1
2
|
`sun.misc.Launcher$AppClassLoader@9304b1`
`sun.misc.Launcher$ExtClassLoader@190d11`
|
如?[代碼清單 2](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code2)所示,第一個輸出的是?`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`。下面通過示例來具體說明。[代碼清單 3](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code3)中給出了 Java 類?`com.example.Sample`。
##### 清單 3\. com.example.Sample 類
|
1
2
3
4
5
6
7
8
9
|
`package com.example;`
`public class Sample {`
`private Sample instance;`
`public void setSample(Object instance) {`
`this.instance = (Sample) instance;`
`}`
`}`
|
如?[代碼清單 3](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code3)所示,`com.example.Sample`類的方法?`setSample`接受一個?`java.lang.Object`類型的參數,并且會把該參數強制轉換成?`com.example.Sample`類型。測試 Java 類是否相同的代碼如?[代碼清單 4](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code4)所示。
##### 清單 4\. 測試 Java 類是否相同
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
`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();`
`}`
`}`
|
[代碼清單 4](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code4)中使用了類?`FileSystemClassLoader`的兩個不同實例來分別加載類?`com.example.Sample`,得到了兩個不同的?`java.lang.Class`的實例,接著通過?`newInstance()`方法分別生成了兩個類的對象?`obj1`和?`obj2`,最后通過 Java 的反射 API 在對象?`obj1`上調用方法?`setSample`,試圖把對象?`obj2`賦值給?`obj1`內部的?`instance`對象。[代碼清單 4](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code4)的運行結果如?[代碼清單 5](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code5)所示。
##### 清單 5\. 測試 Java 類是否相同的運行結果
|
1
2
3
4
5
6
7
8
9
10
11
|
`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`
|
從?[代碼清單 5](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code5)給出的運行結果可以看到,運行時拋出了?`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`來實現的。前者稱為一個類的定義加載器(defining loader),后者稱為初始加載器(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`方法不會被重復調用。
下面討論另外一種類加載器:線程上下文類加載器。
### 線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類?`java.lang.Thread`中的方法?`getContextClassLoader()`和?`setContextClassLoader(ClassLoader cl)`用來獲取和設置線程的上下文類加載器。如果沒有通過?`setContextClassLoader(ClassLoader cl)`方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統類加載器。在線程中運行的代碼可以通過此類加載器來加載類和資源。
前面提到的類加載器的代理模式并不能解決 Java 應用開發中會遇到的類加載器的全部問題。Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在?`javax.xml.parsers`包中。這些 SPI 的實現代碼很可能是作為 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的?[Apache Xerces](http://xerces.apache.org/)所包含的 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`是一個靜態方法,同樣可以用來加載類。該方法有兩種形式:`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 數據庫的驅動。
在介紹完類加載器相關的基本概念之后,下面介紹如何開發自己的類加載器。
## 開發自己的類加載器
雖然在絕大多數情況下,系統默認提供的類加載器實現已經可以滿足需求。但是在某些情況下,您還是需要為應用開發出自己的類加載器。比如您的應用通過網絡來傳輸 Java 類的字節代碼,為了保證安全性,這些字節代碼經過了加密處理。這個時候您就需要自己的類加載器來從某個網絡地址上讀取加密后的字節代碼,接著進行解密和驗證,最后定義出要在 Java 虛擬機中運行的類來。下面將通過兩個具體的實例來說明類加載器的開發。
### 文件系統類加載器
第一個類加載器用來加載存儲在文件系統上的 Java 字節代碼。完整的實現如?[代碼清單 6](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)所示。
##### 清單 6\. 文件系統類加載器
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
`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";`
`}`
`}`
|
如?[代碼清單 6](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)所示,類?`FileSystemClassLoader`繼承自類?`java.lang.ClassLoader`。在?[表 1](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#minor1.1)中列出的?`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 類的方法。而使用接口的做法則是把接口的類放在客戶端中,從服務器上加載實現此接口的不同版本的類。在客戶端通過相同的接口來使用這些實現類。網絡類加載器的具體代碼見?[下載](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#artdownload)。
在介紹完如何開發自己的類加載器之后,下面說明類加載器和 Web 容器的關系。
## 類加載器與 Web 容器
對于運行在 Java EE?容器中的 Web 應用來說,類加載器的實現方式與一般的 Java 應用有所不同。不同的 Web 容器的實現方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規范中的推薦做法,其目的是使得 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`等異常的時候,應該檢查拋出異常的類的類加載器和當前線程的上下文類加載器,從中可以發現問題的所在。在開發自己的類加載器的時候,需要注意與已有的類加載器組織結構的協調。
#### 下載資源
* [類加載器示例代碼](http://www.ibm.com/developerworks/apps/download/index.jsp?contentid=470295&filename=classloader.zip&method=http&locale=zh_CN)?(classloader.zip | 13 KB)
#### 相關主題
* “[The Java Language Specification](http://java.sun.com/docs/books/jls/)”的第 12 章“[Execution](http://java.sun.com/docs/books/jls/third_edition/html/execution.html)”和“[The Java Virtual Machine Specification](http://java.sun.com/docs/books/jvms/)”的第 5 章“[Loading, Linking, and Initializing](http://java.sun.com/docs/books/jvms/second_edition/html/ConstantPool.doc.html)” 詳細介紹了 Java 類的加載、鏈接和初始化。
* “[OSGi Service Platform Core Specification](http://www.osgi.org/Specifications/HomePage)”:OSGi 規范文檔
* “[The Apache Tomcat 5.5 Servlet/JSP Container - Class Loader HOW-TO](http://tomcat.apache.org/tomcat-5.5-doc/class-loader-howto.html)”:詳細介紹了 Tomcat 5.5 中的類加載器機制。
* [developerWorks Java 技術專區](http://www.ibm.com/developerworks/cn/java/):數百篇關于 Java 編程各個方面的文章。
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)