類加載器
是 Java 語言的一個創(chuàng)新苇侵,也是 Java 語言流行的重要原因之一著摔。它使得 Java 類可以被動態(tài)加載到 Java 虛擬機中并執(zhí)行。類加載器從 JDK 1.0 就出現(xiàn)了泄伪,最初是為了滿足 Java Applet 的需要而開發(fā)出來的失都。Java Applet 需要從遠程下載 Java 類文件到瀏覽器中并執(zhí)行。現(xiàn)在類加載器在 Web 容器和 OSGi 中得到了廣泛的使用庙曙。一般來說空镜,Java 應用的開發(fā)人員不需要直接同類加載器進行交互。Java 虛擬機默認的行為就已經(jīng)足夠滿足大多數(shù)情況的需求了捌朴。不過如果遇到了需要與類加載器進行交互的情況吴攒,而對類加載器的機制又不是很了解的話,就很容易花大量的時間去調(diào)試 ClassNotFoundException和 NoClassDefFoundError等異常男旗。本文將詳細介紹 Java 的類加載器舶斧,幫助讀者深刻理解 Java 語言中的這個重要概念。下面首先介紹一些相關的基本概念察皇。
類加載器基本概念
顧名思義茴厉,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。一般來說什荣,Java 虛擬機使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉(zhuǎn)換成 Java 字節(jié)代碼(.class 文件)
矾缓。類加載器負責讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class類的一個實例稻爬。每個這樣的實例用來表示一個 Java 類嗜闻。通過此實例的 newInstance()方法就可以創(chuàng)建出該類的一個對象。實際的情況可能更加復雜桅锄,比如 Java 字節(jié)代碼可能是通過工具動態(tài)生成的琉雳,也可能是通過網(wǎng)絡下載的样眠。
基本上所有的類加載器都是 java.lang.ClassLoader類的一個實例。下面詳細介紹這個 Java 類翠肘。
java.lang.ClassLoader類介紹
java.lang.ClassLoader
類的基本職責就是根據(jù)一個指定的類的名稱檐束,找到或者生成其對應的字節(jié)代碼,然后從這些字節(jié)代碼中定義出一個 Java 類束倍,即 java.lang.Class類的一個實例被丧。除此之外,ClassLoader還負責加載 Java 應用所需的資源绪妹,如圖像文件和配置文件等甥桂。不過本文只討論其加載類的功能。為了完成加載類的這個職責邮旷,ClassLoader提供了一系列的方法黄选,關于這些方法的細節(jié)會在下面進行介紹。
- getParent() 返回該類加載器的父類加載器廊移。
- loadClass(String name) 加載名稱為 name的類糕簿,返回的結(jié)果是 java.lang.Class類的實例。
- findClass(String name) 查找名稱為 name的類狡孔,返回的結(jié)果是 java.lang.Class類的實例懂诗。
- findLoadedClass(String name) 查找名稱為 name的已經(jīng)被加載過的類,返回的結(jié)果是 java.lang.Class類的實例苗膝。
- defineClass(String name, byte[] b, int off, int len) 把字節(jié)數(shù)組 b中的內(nèi)容轉(zhuǎn)換成 Java 類殃恒,返回的結(jié)果是 java.lang.Class類的實例。這個方法被聲明為 final的辱揭。
- resolveClass(Class<?> c) 鏈接指定的 Java 類。
對于上面給出的方法问窃,表示類名稱的 name參數(shù)的值是類的二進制名稱。需要注意的是內(nèi)部類的表示域庇,如 com.example.Sample$1和 com.example.Sample$Inner等表示方式。這些方法會在下面介紹類加載器的工作機制時熟呛,做進一步的說明。
類加載器的樹狀組織結(jié)構庵朝。
Java 中的類加載器大致可以分成兩類,一類是系統(tǒng)提供的九府,另外一類則是由 Java 應用開發(fā)人員編寫的。系統(tǒng)提供的類加載器主要有下面三個:
- 引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫昔逗,是用原生代碼來實現(xiàn)的,并不繼承自 java.lang.ClassLoader。
- 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫声旺。Java 虛擬機的實現(xiàn)會提供一個擴展庫目錄笔链。該類加載器在此目錄里面查找并加載 Java 類。
- 系統(tǒng)類加載器(system class loader):它根據(jù) Java 應用的類路徑(CLASSPATH)來加載 Java 類腮猖。一般來說鉴扫,Java 應用的類都是由它來完成加載的〕喝保可以通過 ClassLoader.getSystemClassLoader()來獲取它坪创。
除了系統(tǒng)提供的類加載器以外,開發(fā)人員可以通過繼承 java.lang.ClassLoader
類的方式實現(xiàn)自己的類加載器姐赡,以滿足一些特殊的需求莱预。
除了引導類加載器之外,所有的類加載器都有一個父類加載器项滑。通過 getParent()方法可以得到依沮。對于系統(tǒng)提供的類加載器來說,系統(tǒng)類加載器的父類加載器是擴展類加載器枪狂,而擴展類加載器的父類加載器是引導類加載器
危喉;對于開發(fā)人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器州疾。因為類加載器 Java 類如同其它的 Java 類一樣辜限,也是要由類加載器來加載的。一般來說严蓖,開發(fā)人員編寫的類加載器的父類加載器是系統(tǒng)類加載器薄嫡。類加載器通過這種方式組織起來,形成樹狀結(jié)構谈飒。樹的根節(jié)點就是引導類加載器岂座。圖 1中給出了一個典型的類加載器樹狀組織結(jié)構示意圖,其中的箭頭指向的是父類加載器杭措。
清單 1. 演示類加載器的樹狀組織結(jié)構
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 中通過遞歸調(diào)用 getParent()方法來輸出全部的父類加載器。代碼清單 1的運行結(jié)果如 代碼清單 2 所示鸳址。
sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11
如 代碼清單 2 所示瘩蚪,第一個輸出的是 ClassLoaderTree類的類加載器疹瘦,即系統(tǒng)類加載器巡球。它是 sun.misc.Launcher$AppClassLoader類的實例酣栈;第二個輸出的是擴展類加載器矿筝,是 sun.misc.Launcher$ExtClassLoader類的實例。需要注意的是這里并沒有輸出引導類加載器榆综,這是由于有些 JDK 的實現(xiàn)對于父類加載器是引導類加載器的情況鼻疮,getParent()方法返回 null陋守。
在了解了類加載器的樹狀組織結(jié)構之后水评,下面介紹類加載器的代理模式媚送。
類加載器在嘗試自己去查找某個類的字節(jié)代碼并定義它時塘偎,會先代理給其父類加載器吟秩,由父類加載器先去嘗試加載這個類,依次類推闹伪。在介紹代理模式背后的動機之前偏瓤,首先需要說明一下 Java 虛擬機是如何判定兩個 Java 類是相同的厅克。Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣
硕旗。只有兩者都相同的情況卵渴,才認為兩個類是相同的。即便是同樣的字節(jié)代碼辛藻,被不同的類加載器加載之后所得到的類互订,也是不同的
仰禽。
比如一個 java類 com.example.Sample吐葵,編譯之后生成了字節(jié)代碼文件 Sample.class。兩個不同的類加載器 ClassLoaderA和 ClassLoaderB分別讀取了這個 Sample.class文件猛铅,并定義出兩個 java.lang.Class類的實例來表示這個類奸忽。這兩個實例是不相同的栗菜。對于 Java 虛擬機來說疙筹,它們是不同的類。試圖對這兩個類的對象進行相互賦值蛙酪,會拋出運行時異常 ClassCastException桂塞。
下面通過示例來具體說明阁危。代碼清單 3 中給出了 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;
}
}
如 代碼清單 3 所示趴乡,com.example.Sample類的方法 setSample接受一個 java.lang.Object類型的參數(shù)晾捏,并且會把該參數(shù)強制轉(zhuǎn)換成 com.example.Sample類型哀托。
測試 Java 類是否相同的代碼如 代碼清單 4 所示仓手。
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中使用了類 FileSystemClassLoader的兩個不同實例來分別加載類 com.example.Sample呀伙,得到了兩個不同的 java.lang.Class的實例辛慰,接著通過 newInstance()方法分別生成了兩個類的對象 obj1和 obj2帅腌,最后通過 Java 的反射 API 在對象 obj1上調(diào)用方法 setSample速客,試圖把對象 obj2賦值給 obj1內(nèi)部的 instance對象。代碼清單 4 的運行結(jié)果如 代碼清單 5 所示岔擂。
清單 5. 測試 Java 類是否相同的運行結(jié)果
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 給出的運行結(jié)果可以看到乱灵,運行時了 java.lang.ClassCastException異常痛倚。雖然兩個對象 obj1和 obj2的類的名字相同,但是這兩個類是由不同的類加載器實例來加載的抒蚜,因此不被 Java 虛擬機認為是相同的
嗡髓。
了解了這一點之后饿这,就可以理解代理模式的設計動機了蛹稍。代理模式是為了保證 Java 核心庫的類型安全
部服。所有 Java 應用都至少需要引用 java.lang.Object類也就是說在運行的時候廓八,java.lang.Object這個類需要被加載到 Java 虛擬機中剧蹂。
如果這個加載過程由 Java 應用自己的類加載器來完成的話宠叼,很可能就存在多個版本的 java.lang.Object類冒冬,而且這些類之間是不兼容的简烤。
通過代理模式摇幻,對于 Java 核心庫的類的加載工作由引導類加載器來統(tǒng)一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類枉侧,是互相兼容的榨馁。
不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間辆影。相同名稱的類可以并存在 Java 虛擬機中,只需要用不同的類加載器來加載它們即可锯蛀。不同類加載器加載的類之間是不兼容的旁涤,這就相當于在 Java 虛擬機內(nèi)部創(chuàng)建了一個個相互隔離的 Java 類空間劈愚。這種技術在許多框架中都被用到菌羽,后面會詳細介紹注祖。
下面具體介紹類加載器加載類的詳細過程均唉。加載類的過程
在前面介紹類加載器的代理模式的時候舔箭,提到過類加載器會首先代理給其它類加載器來嘗試加載某個類层扶。這就意味著真正完成類的加載工作的類加載器和啟動這個加載過程的類加載器
,有可能不是同一個炉抒。真正完成類的加載工作是通過調(diào)用 defineClass來實現(xiàn)的焰薄;而啟動類的加載過程是通過調(diào)用 loadClass來實現(xiàn)的。前者稱為一個類的定義加載器(defining loader)亩码,后者稱為初始加載器(initiating loader)
描沟。在 Java 虛擬機判斷兩個類是否相同的時候吏廉,使用的是類的定義加載器惰许。也就是說汹买,哪個類加載器啟動類的加載過程并不重要晦毙,重要的是最終定義這個類的加載器见妒。
兩種類加載器的關聯(lián)之處在于:一個類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer引用了類 com.example.Inner垃环,則由類 com.example.Outer的定義加載器負責啟動類 com.example.Inner的加載過程。
方法 loadClass()拋出的是 java.lang.ClassNotFoundException異常劲赠;方法 defineClass()拋出的是 java.lang.NoClassDefFoundError異常凛澎。
類加載器在成功加載某個類之后估蹄,會把得到的 java.lang.Class類的實例緩存起來臭蚁。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例冷尉,而不會嘗試再次加載雀哨。也就是說,對于一個類加載器實例來說膊夹,相同全名的類只加載一次放刨,即 loadClass方法不會被重復調(diào)用
宏榕。
下面討論另外一種類加載器:線程上下文類加載器麻昼。
線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入類 java.lang.Thread中的方法getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用來獲取和設置線程的上下文類加載器抚芦。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設置的話叉抡,線程將繼承其父線程的上下文類加載器答毫。Java 應用運行的初始線程的上下文類加載器是系統(tǒng)類加載器洗搂。在線程中運行的代碼可以通過此類加載器來加載類和資源耘拇。
前面提到的類加載器的代理模式并不能解決 Java 應用開發(fā)中會遇到的類加載器的全部問題。
Java 提供了很多服務提供者接口(Service Provider Interface倡勇,SPI)妻熊,允許第三方為這些接口提供實現(xiàn)。常見的 SPI 有 JDBC题篷、JCE番枚、JNDI葫笼、JAXP 和 JBI 等拗馒。這些 SPI 的接口由 Java 核心庫來提供诱桂,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers包中挥等。這些 SPI 的實現(xiàn)代碼很可能是作為 Java 應用所依賴的 jar 包被包含進來肝劲,可以通過類路徑(CLASSPATH)來找到,如實現(xiàn)了 JAXP SPI 的 Apache Xerces所包含的 jar 包掷漱。SPI 接口中的代碼經(jīng)常需要加載具體的實現(xiàn)類卜范。如 JAXP 的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的 DocumentBuilderFactory的實例先朦。這里的實例的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現(xiàn)所提供的棉浸。
如在 Apache Xerces 中迷郑,實現(xiàn)的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而問題在于焚碌,SPI 的接口是 Java 核心庫的一部分十电,是由引導類加載器來加載的鹃骂;SPI 實現(xiàn)的 Java 類一般是由系統(tǒng)類加載器來加載的。引導類加載器是無法找到 SPI 的實現(xiàn)類的静盅,因為它只加載 Java 的核心庫蒿叠。它也不能代理給系統(tǒng)類加載器市咽,因為它是系統(tǒng)類加載器的祖先類加載器魂务。也就是說泌射,類加載器的代理模式無法解決這個問題熔酷。
線程上下文類加載器正好解決了這個問題拒秘。如果不做任何的設置躺酒,Java 應用的線程的上下文類加載器默認就是系統(tǒng)上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器揽碘,就可以成功的加載到 SPI 實現(xiàn)的類雳刺。線程上下文類加載器在很多 SPI 的實現(xiàn)中都會用到掖桦。
下面介紹另外一種加載類的方法:
Class.forName
枪汪。
Class.forName
Class.forName是一個靜態(tài)方法,同樣可以用來加載類
蒲犬。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)原叮。第一種形式的參數(shù)name表示的是類的全名奋隶;initialize表示是否初始化類唯欣;loader表示加載時使用的類加載器境氢。第二種形式則相當于設置了參數(shù) initialize的值為 true萍聊,loader的值為當前類的類加載器悦析。 Class.forName的一個很常見的用法是在加載數(shù)據(jù)庫驅(qū)動的時候强戴。
Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() 用來加載 Apache Derby 數(shù)據(jù)庫的驅(qū)動骑歹。
在介紹完類加載器相關的基本概念之后道媚,下面介紹如何開發(fā)自己的類加載器欢嘿。
開發(fā)自己的類加載器
雖然在絕大多數(shù)情況下,系統(tǒng)默認提供的類加載器實現(xiàn)已經(jīng)可以滿足需求狸剃。但是在某些情況下狗热,您還是需要為應用開發(fā)出自己的類加載器匿刮。比如您的應用通過網(wǎng)絡來傳輸 Java 類的字節(jié)代碼,為了保證安全性训措,這些字節(jié)代碼經(jīng)過了加密處理绩鸣。這個時候您就需要自己的類加載器來從某個網(wǎng)絡地址上讀取加密后的字節(jié)代碼纱兑,接著進行解密和驗證潜慎,最后定義出要在 Java 虛擬機中運行的類來铐炫。下面將通過兩個具體的實例來說明類加載器的開發(fā)驳遵。
文件系統(tǒng)類加載器
第一個類加載器用來加載存儲在文件系統(tǒng)上的 Java 字節(jié)代碼。完整的實現(xiàn)如 代碼清單 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";
}
}
如 代碼清單 6 所示,類 FileSystemClassLoader繼承自類 java.lang.ClassLoader瘾带。在 上面中列出的 java.lang.ClassLoader類的常用方法中,一般來說抄罕,自己開發(fā)的類加載器只需要覆寫 findClass(String name)方法即可
呆贿。java.lang.ClassLoader類的方法 loadClass()封裝了前面提到的代理模式的實現(xiàn)森渐。該方法會首先調(diào)用 findLoadedClass()方法來檢查該類是否已經(jīng)被加載過;如果沒有加載過的話竟块,會調(diào)用父類加載器的 loadClass()方法來嘗試加載該類浪秘;如果父類加載器無法加載該類的話秫逝,就調(diào)用 findClass()方法來查找類询枚。因此金蜀,為了保證類加載器都正確實現(xiàn)代理模式渊抄,在開發(fā)自己的類加載器時,最好不要覆寫 loadClass()方法含衔,而是覆寫 findClass()方法贪染。
類 FileSystemClassLoader的 findClass()方法首先根據(jù)類的全名在硬盤上查找類的字節(jié)代碼文件(.class 文件)杭隙,然后讀取該文件內(nèi)容,最后通過 defineClass()方法來把這些字節(jié)代碼轉(zhuǎn)換成 java.lang.Class類的實例痰憎。
網(wǎng)絡類加載器
下面將通過一個網(wǎng)絡類加載器來說明如何通過類加載器來實現(xiàn)組件的動態(tài)更新。即基本的場景是:Java 字節(jié)代碼(.class)文件存放在服務器上铣耘,客戶端通過網(wǎng)絡的方式獲取字節(jié)代碼并執(zhí)行。當有版本更新的時候玲躯,只需要替換掉服務器上保存的文件即可。通過類加載器可以比較簡單的實現(xiàn)這種需求棘利。
類 NetworkClassLoader
負責通過網(wǎng)絡下載 Java 類字節(jié)代碼并定義出 Java 類水援。它的實現(xiàn)與 FileSystemClassLoader類似蜗元。在通過 NetworkClassLoader加載了某個版本的類之后系冗,一般有兩種做法來使用它掌敬。第一種做法是使用 Java 反射 API奔害。另外一種做法是使用接口
。需要注意的是芯杀,并不能直接在客戶端代碼中引用從服務器上下載的類揭厚,因為客戶端代碼的類加載器找不到這些類棋弥。使用 Java 反射 API 可以直接調(diào)用 Java 類的方法顽染。而使用接口的做法則是把接口的類放在客戶端中粉寞,從服務器上加載實現(xiàn)此接口的不同版本的類唧垦。在客戶端通過相同的接口來使用這些實現(xiàn)類。
在介紹完如何開發(fā)自己的類加載器之后巧还,下面說明類加載器和 Web 容器的關系麸祷。
對于運行在 Java EE?容器中的 Web 應用來說阶牍,類加載器的實現(xiàn)方式與一般的 Java 應用有所不同星瘾。不同的 Web 容器的實現(xiàn)方式也會有所不同琳状。以 Apache Tomcat 來說念逞,每個 Web 應用都有一個對應的類加載器實例肮柜。該類加載器也使用代理模式审洞,所不同的是它是首先嘗試去加載某個類芒澜,如果找不到再代理給父類加載器痴晦。這與一般類加載器的順序是相反的誊酌。這是 Java Servlet 規(guī)范中的推薦做法露乏,其目的是使得 Web 應用自己的類的優(yōu)先級高于 Web 容器提供的類瘟仿。這種代理模式的一個例外是:Java 核心庫的類是不在查找范圍之內(nèi)的。這也是為了保證 Java 核心庫的類型安全比勉。
絕大多數(shù)情況下劳较,Web 應用的開發(fā)人員不需要考慮與類加載器相關的細節(jié)。下面給出幾條簡單的原則:
- 每個 Web 應用自己的 Java 類文件和使用的庫的 jar 包浩聋,分別放在 WEB-INF/classes和 WEB-INF/lib目錄下面观蜗。
- 多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面衣洁。
- 當出現(xiàn)找不到類的錯誤時嫂便,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。
在介紹完類加載器與 Web 容器的關系之后闸与,下面介紹它與 OSGi 的關系。
OSGi? 是 Java 上的動態(tài)模塊系統(tǒng)。它為開發(fā)人員提供了面向服務和基于組件的運行環(huán)境拷邢,并提供標準的方式用來管理軟件的生命周期腻惠。OSGi 已經(jīng)被實現(xiàn)和部署在很多產(chǎn)品上悔雹,在開源社區(qū)也得到了廣泛的支持益涧。Eclipse 就是基于 OSGi 技術來構建的久免。
OSGi 中的每個模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導入(import)的其它模塊的 Java 包和類(通過Import-Package),也可以聲明導出(export)自己的包和類,供其它模塊使用(通過 Export-Package)屑埋。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類团搞。這是通過 OSGi 特有的類加載器機制來實現(xiàn)的。OSGi 中的每個模塊都有對應的一個類加載器。它負責加載模塊自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java開頭的包和類)轻局,它會代理給父類加載器(通常是啟動類加載器)來完成置鼻。當它需要加載所導入的 Java 類時俱济,它會代理給導出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設置系統(tǒng)屬性 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 在其模塊內(nèi)部查找類 com.bundleA.Sample并定義它,所得到的類 com.bundleA.Sample實例就可以被所有聲明導入了此類的模塊使用涨缚。對于以 java開頭的類盯荤,都是由父類加載器來加載的。如果聲明了系統(tǒng)屬性 org.osgi.framework.bootdelegation=com.example.core.*,那么對于包 com.example.core中的類级野,都是由父類加載器來完成的牢贸。
OSGi 模塊的這種類加載器結(jié)構,使得一個類的不同版本可以共存在 Java 虛擬機中芍锚,帶來了很大的靈活性荤西。不過它的這種不同饵溅,也會給開發(fā)人員帶來一些麻煩懦底,尤其當模塊需要使用第三方提供的庫的時候弥臼。下面提供幾條比較好的建議:
- 如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中搏明,在 Bundle-ClassPath中指明即可同欠。
- 如果一個類庫被多個模塊共用炕檩,可以為這個類庫單獨的創(chuàng)建一個模塊,把其它模塊需要用到的 Java 包聲明為導出的丁存。其它模塊聲明導入這些類夫偶。
- 如果類庫提供了 SPI 接口嘹履,并且利用線程上下文類加載器來加載 SPI 實現(xiàn)的 Java 類,有可能會找不到 Java 類。如果出現(xiàn)了 NoClassDefFoundError異常召夹,首先檢查當前線程的上下文類加載器是否正確。通過 Thread.currentThread().getContextClassLoader()就可以得到該類加載器渔扎。該類加載器應該是該模塊對應的類加載器定罢。如果不是的話,可以首先通過 class.getClassLoader()來得到模塊對應的類加載器,再通過 Thread.currentThread().setContextClassLoader()來設置當前線程的上下文類加載器龙屉。
總結(jié)
類加載器是 Java 語言的一個創(chuàng)新与柑。它使得動態(tài)安裝和更新軟件組件成為可能。本文詳細介紹了類加載器的相關話題植榕,包括基本概念拐邪、代理模式啡邑、線程上下文類加載器、與 Web 容器和 OSGi 的關系等球切。開發(fā)人員在遇到 ClassNotFoundException和 NoClassDefFoundError等異常的時候恩商,應該檢查拋出異常的類的類加載器和當前線程的上下文類加載器,從中可以發(fā)現(xiàn)問題的所在。在開發(fā)自己的類加載器的時候魏宽,需要注意與已有的類加載器組織結(jié)構的協(xié)調(diào)。