其他參考:
1 http://gzcj.javaeye.com/blog/394648
2 http://blog.sina.com.cn/s/blog_5f1fe33f0100d9ak.html
類加載器是 Java 語(yǔ)言流行的重要原因之一。它使得 Java 類可以被動(dòng)態(tài)加載到 Java 虛擬機(jī)中并執(zhí)行。類加載器從 JDK 1.0 就出現(xiàn)了,最初是為了滿足 Java Applet 的需要而開(kāi)發(fā)出來(lái)的。Java Applet 需要從遠(yuǎn)程下載 Java 類文件到瀏覽器中并執(zhí)行。現(xiàn)在類加載器在 Web 容器和 OSGi 中得到了廣泛的使用。一般來(lái)說(shuō),Java 應(yīng)用的開(kāi)發(fā)人員不需要直接同類加載器進(jìn)行交互。Java 虛擬機(jī)默認(rèn)的行為就已經(jīng)足夠滿足大多數(shù)情況的需求了。不過(guò)如果遇到了需要與類加載器進(jìn)行交互的情況,而對(duì)類加載器的機(jī)制又不是很了解的話,就很容易花大量的時(shí)間去調(diào)試
ClassNotFoundException
和
NoClassDefFoundError
等異常。本文將詳細(xì)介紹 Java 的類加載器,幫助讀者深刻理解 Java 語(yǔ)言中的這個(gè)重要概念。下面首先介紹一些相關(guān)的基本概念。
類加載器基本概念
顧名思義,類加載器(class loader)用來(lái)加載 Java 類到 Java 虛擬機(jī)中。一般來(lái)說(shuō),Java 虛擬機(jī)使用 Java 類的方式如下:Java 源程序(.java 文件)在經(jīng)過(guò) Java 編譯器編譯之后就被轉(zhuǎn)換成 Java 字節(jié)代碼(.class 文件)。類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class
類的一個(gè)實(shí)例。每個(gè)這樣的實(shí)例用來(lái)表示一個(gè) Java 類。通過(guò)此實(shí)例的 newInstance()
方法就可以創(chuàng)建出該類的一個(gè)對(duì)象。實(shí)際的情況可能更加復(fù)雜,比如 Java 字節(jié)代碼可能是通過(guò)工具動(dòng)態(tài)生成的,也可能是通過(guò)網(wǎng)絡(luò)下載的。
基本上所有的類加載器都是 java.lang.ClassLoader
類的一個(gè)實(shí)例。下面詳細(xì)介紹這個(gè) Java 類。
java.lang.ClassLoader
類介紹
java.lang.ClassLoader
類的基本職責(zé)就是根據(jù)一個(gè)指定的類的名稱,找到或者生成其對(duì)應(yīng)的字節(jié)代碼,然后從這些字節(jié)代碼中定義出一個(gè) Java 類,即 java.lang.Class
類的一個(gè)實(shí)例。除此之外,ClassLoader
還負(fù)責(zé)加載 Java 應(yīng)用所需的資源,如圖像文件和配置文件等。不過(guò)本文只討論其加載類的功能。為了完成加載類的這個(gè)職責(zé),ClassLoader
提供了一系列的方法,比較重要的方法如 表 1 所示。關(guān)于這些方法的細(xì)節(jié)會(huì)在下面進(jìn)行介紹。
表 1. ClassLoader 中與加載類相關(guān)的方法
方法 |
說(shuō)明 |
getParent() |
返回該類加載器的父類加載器。 |
loadClass(String name) |
加載名稱為 name 的類,返回的結(jié)果是 java.lang.Class 類的實(shí)例。 |
findClass(String name) |
查找名稱為 name 的類,返回的結(jié)果是 java.lang.Class 類的實(shí)例。 |
findLoadedClass(String name) |
查找名稱為 name 的已經(jīng)被加載過(guò)的類,返回的結(jié)果是 java.lang.Class 類的實(shí)例。 |
defineClass(String name, byte[] b, int off, int len) |
把字節(jié)數(shù)組 b 中的內(nèi)容轉(zhuǎn)換成 Java 類,返回的結(jié)果是 java.lang.Class 類的實(shí)例。這個(gè)方法被聲明為 final 的。 |
resolveClass(Class<?> c) |
鏈接指定的 Java 類。 |
對(duì)于 表 1 中給出的方法,表示類名稱的 name
參數(shù)的值是類的二進(jìn)制名稱。需要注意的是內(nèi)部類的表示,如 com.example.Sample$1
和 com.example.Sample$Inner
等表示方式。這些方法會(huì)在下面介紹類加載器的工作機(jī)制時(shí),做進(jìn)一步的說(shuō)明。下面介紹類加載器的樹(shù)狀組織結(jié)構(gòu)。
類加載器的樹(shù)狀組織結(jié)構(gòu)
Java 中的類加載器大致可以分成兩類,一類是系統(tǒng)提供的,另外一類則是由 Java 應(yīng)用開(kāi)發(fā)人員編寫(xiě)的。系統(tǒng)提供的類加載器主要有下面三個(gè):
- 引導(dǎo)類加載器(bootstrap class loader):它用來(lái)加載 Java 的核心庫(kù),是用原生代碼來(lái)實(shí)現(xiàn)的,并不繼承自
java.lang.ClassLoader
。
- 擴(kuò)展類加載器(extensions class loader):它用來(lái)加載 Java 的擴(kuò)展庫(kù)。Java 虛擬機(jī)的實(shí)現(xiàn)會(huì)提供一個(gè)擴(kuò)展庫(kù)目錄。該類加載器在此目錄里面查找并加載 Java 類。
- 系統(tǒng)類加載器(system class loader):它根據(jù) Java 應(yīng)用的類路徑(CLASSPATH)來(lái)加載 Java 類。一般來(lái)說(shuō),Java 應(yīng)用的類都是由它來(lái)完成加載的。可以通過(guò)
ClassLoader.getSystemClassLoader()
來(lái)獲取它。
除了系統(tǒng)提供的類加載器以外,開(kāi)發(fā)人員可以通過(guò)繼承 java.lang.ClassLoader
類的方式實(shí)現(xiàn)自己的類加載器,以滿足一些特殊的需求。
除了引導(dǎo)類加載器之外,所有的類加載器都有一個(gè)父類加載器。通過(guò) 表 1 中給出的 getParent()
方法可以得到。對(duì)于系統(tǒng)提供的類加載器來(lái)說(shuō),系統(tǒng)類加載器的父類加載器是擴(kuò)展類加載器,而擴(kuò)展類加載器的父類加載器是引導(dǎo)類加載器;對(duì)于開(kāi)發(fā)人員編寫(xiě)的類加載器來(lái)說(shuō),其父類加載器是加載此類加載器 Java 類的類加載器。因?yàn)轭惣虞d器 Java 類如同其它的 Java 類一樣,也是要由類加載器來(lái)加載的。一般來(lái)說(shuō),開(kāi)發(fā)人員編寫(xiě)的類加載器的父類加載器是系統(tǒng)類加載器。類加載器通過(guò)這種方式組織起來(lái),形成樹(shù)狀結(jié)構(gòu)。樹(shù)的根節(jié)點(diǎn)就是引導(dǎo)類加載器。圖 1 中給出了一個(gè)典型的類加載器樹(shù)狀組織結(jié)構(gòu)示意圖,其中的箭頭指向的是父類加載器。
圖 1. 類加載器樹(shù)狀組織結(jié)構(gòu)示意圖
代碼清單 1 演示了類加載器的樹(shù)狀組織結(jié)構(gòu)。
清單 1. 演示類加載器的樹(shù)狀組織結(jié)構(gòu)
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();
}
}
}
|
每個(gè) Java 類都維護(hù)著一個(gè)指向定義它的類加載器的引用,通過(guò) getClassLoader()
方法就可以獲取到此引用。代碼清單 1 中通過(guò)遞歸調(diào)用 getParent()
方法來(lái)輸出全部的父類加載器。代碼清單 1 的運(yùn)行結(jié)果如 代碼清單 2 所示。
清單 2. 演示類加載器的樹(shù)狀組織結(jié)構(gòu)的運(yùn)行結(jié)果
sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11
|
如 代碼清單 2 所示,第一個(gè)輸出的是 ClassLoaderTree
類的類加載器,即系統(tǒng)類加載器。它是 sun.misc.Launcher$AppClassLoader
類的實(shí)例;第二個(gè)輸出的是擴(kuò)展類加載器,是 sun.misc.Launcher$ExtClassLoader
類的實(shí)例。需要注意的是這里并沒(méi)有輸出引導(dǎo)類加載器,這是由于有些 JDK 的實(shí)現(xiàn)對(duì)于父類加載器是引導(dǎo)類加載器的情況,getParent()
方法返回 null
。
在了解了類加載器的樹(shù)狀組織結(jié)構(gòu)之后,下面介紹類加載器的代理模式。
類加載器的代理模式
類加載器在嘗試自己去查找某個(gè)類的字節(jié)代碼并定義它時(shí),會(huì)先代理給其父類加載器,由父類加載器先去嘗試加載這個(gè)類,依次類推。在介紹代理模式背后的動(dòng)機(jī)之前,首先需要說(shuō)明一下 Java 虛擬機(jī)是如何判定兩個(gè) Java 類是相同的。Java 虛擬機(jī)不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認(rèn)為兩個(gè)類是相同的。即便是同樣的字節(jié)代碼,被不同的類加載器加載之后所得到的類,也是不同的。比如一個(gè) Java 類 com.example.Sample
,編譯之后生成了字節(jié)代碼文件 Sample.class
。兩個(gè)不同的類加載器 ClassLoaderA
和 ClassLoaderB
分別讀取了這個(gè) Sample.class
文件,并定義出兩個(gè) java.lang.Class
類的實(shí)例來(lái)表示這個(gè)類。這兩個(gè)實(shí)例是不相同的。對(duì)于 Java 虛擬機(jī)來(lái)說(shuō),它們是不同的類。試圖對(duì)這兩個(gè)類的對(duì)象進(jìn)行相互賦值,會(huì)拋出運(yùn)行時(shí)異常 ClassCastException
。下面通過(guò)示例來(lái)具體說(shuō)明。代碼清單 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
接受一個(gè) java.lang.Object
類型的參數(shù),并且會(huì)把該參數(shù)強(qiáng)制轉(zhuǎn)換成 com.example.Sample
類型。測(cè)試 Java 類是否相同的代碼如 代碼清單 4 所示。
清單 4. 測(cè)試 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();
}
}
|
代碼清單 4 中使用了類 FileSystemClassLoader
的兩個(gè)不同實(shí)例來(lái)分別加載類 com.example.Sample
,得到了兩個(gè)不同的 java.lang.Class
的實(shí)例,接著通過(guò) newInstance()
方法分別生成了兩個(gè)類的對(duì)象 obj1
和 obj2
,最后通過(guò) Java 的反射 API 在對(duì)象 obj1
上調(diào)用方法 setSample
,試圖把對(duì)象 obj2
賦值給 obj1
內(nèi)部的 instance
對(duì)象。代碼清單 4 的運(yùn)行結(jié)果如 代碼清單 5 所示。
清單 5. 測(cè)試 Java 類是否相同的運(yùn)行結(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 給出的運(yùn)行結(jié)果可以看到,運(yùn)行時(shí)拋出了 java.lang.ClassCastException
異常。雖然兩個(gè)對(duì)象 obj1
和 obj2
的類的名字相同,但是這兩個(gè)類是由不同的類加載器實(shí)例來(lái)加載的,因此不被 Java 虛擬機(jī)認(rèn)為是相同的。
了解了這一點(diǎn)之后,就可以理解代理模式的設(shè)計(jì)動(dòng)機(jī)了。代理模式是為了保證 Java 核心庫(kù)的類型安全。所有 Java 應(yīng)用都至少需要引用 java.lang.Object
類,也就是說(shuō)在運(yùn)行的時(shí)候,java.lang.Object
這個(gè)類需要被加載到 Java 虛擬機(jī)中。如果這個(gè)加載過(guò)程由 Java 應(yīng)用自己的類加載器來(lái)完成的話,很可能就存在多個(gè)版本的 java.lang.Object
類,而且這些類之間是不兼容的。通過(guò)代理模式,對(duì)于 Java 核心庫(kù)的類的加載工作由引導(dǎo)類加載器來(lái)統(tǒng)一完成,保證了 Java 應(yīng)用所使用的都是同一個(gè)版本的 Java 核心庫(kù)的類,是互相兼容的。
不同的類加載器為相同名稱的類創(chuàng)建了額外的名稱空間。相同名稱的類可以并存在 Java 虛擬機(jī)中,只需要用不同的類加載器來(lái)加載它們即可。不同類加載器加載的類之間是不兼容的,這就相當(dāng)于在 Java 虛擬機(jī)內(nèi)部創(chuàng)建了一個(gè)個(gè)相互隔離的 Java 類空間。這種技術(shù)在許多框架中都被用到,后面會(huì)詳細(xì)介紹。
下面具體介紹類加載器加載類的詳細(xì)過(guò)程。
加載類的過(guò)程
在前面介紹類加載器的代理模式的時(shí)候,提到過(guò)類加載器會(huì)首先代理給其它類加載器來(lái)嘗試加載某個(gè)類。這就意味著真正完成類的加載工作的類加載器和啟動(dòng)這個(gè)加載過(guò)程的類加載器,有可能不是同一個(gè)。真正完成類的加載工作是通過(guò)調(diào)用 defineClass
來(lái)實(shí)現(xiàn)的;而啟動(dòng)類的加載過(guò)程是通過(guò)調(diào)用 loadClass
來(lái)實(shí)現(xiàn)的。前者稱為一個(gè)類的定義加載器(defining loader),后者稱為初始加載器(initiating loader)。在 Java 虛擬機(jī)判斷兩個(gè)類是否相同的時(shí)候,使用的是類的定義加載器。也就是說(shuō),哪個(gè)類加載器啟動(dòng)類的加載過(guò)程并不重要,重要的是最終定義這個(gè)類的加載器。兩種類加載器的關(guān)聯(lián)之處在于:一個(gè)類的定義加載器是它引用的其它類的初始加載器。如類 com.example.Outer
引用了類 com.example.Inner
,則由類 com.example.Outer
的定義加載器負(fù)責(zé)啟動(dòng)類 com.example.Inner
的加載過(guò)程。
方法 loadClass()
拋出的是 java.lang.ClassNotFoundException
異常;方法 defineClass()
拋出的是 java.lang.NoClassDefFoundError
異常。
類加載器在成功加載某個(gè)類之后,會(huì)把得到的 java.lang.Class
類的實(shí)例緩存起來(lái)。下次再請(qǐng)求加載該類的時(shí)候,類加載器會(huì)直接使用緩存的類的實(shí)例,而不會(huì)嘗試再次加載。也就是說(shuō),對(duì)于一個(gè)類加載器實(shí)例來(lái)說(shuō),相同全名的類只加載一次,即 loadClass
方法不會(huì)被重復(fù)調(diào)用。
下面討論另外一種類加載器:線程上下文類加載器。
線程上下文類加載器
線程上下文類加載器(context class loader)是從 JDK 1.2 開(kāi)始引入的。類 java.lang.Thread
中的方法 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用來(lái)獲取和設(shè)置線程的上下文類加載器。如果沒(méi)有通過(guò) setContextClassLoader(ClassLoader cl)
方法進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類加載器。Java 應(yīng)用運(yùn)行的初始線程的上下文類加載器是系統(tǒng)類加載器。在線程中運(yùn)行的代碼可以通過(guò)此類加載器來(lái)加載類和資源。
前面提到的類加載器的代理模式并不能解決 Java 應(yīng)用開(kāi)發(fā)中會(huì)遇到的類加載器的全部問(wèn)題。Java 提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實(shí)現(xiàn)。常見(jiàn)的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫(kù)來(lái)提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers
包中。這些 SPI 的實(shí)現(xiàn)代碼很可能是作為 Java 應(yīng)用所依賴的 jar 包被包含進(jìn)來(lái),可以通過(guò)類路徑(CLASSPATH)來(lái)找到,如實(shí)現(xiàn)了 JAXP SPI 的 Apache Xerces 所包含的 jar 包。SPI 接口中的代碼經(jīng)常需要加載具體的實(shí)現(xiàn)類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory
類中的 newInstance()
方法用來(lái)生成一個(gè)新的 DocumentBuilderFactory
的實(shí)例。這里的實(shí)例的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory
,由 SPI 的實(shí)現(xiàn)所提供的。如在 Apache Xerces 中,實(shí)現(xiàn)的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而問(wèn)題在于,SPI 的接口是 Java 核心庫(kù)的一部分,是由引導(dǎo)類加載器來(lái)加載的;SPI 實(shí)現(xiàn)的 Java 類一般是由系統(tǒng)類加載器來(lái)加載的。引導(dǎo)類加載器是無(wú)法找到 SPI 的實(shí)現(xiàn)類的,因?yàn)樗患虞d Java 的核心庫(kù)。它也不能代理給系統(tǒng)類加載器,因?yàn)樗窍到y(tǒng)類加載器的祖先類加載器。也就是說(shuō),類加載器的代理模式無(wú)法解決這個(gè)問(wèn)題。
線程上下文類加載器正好解決了這個(gè)問(wèn)題。如果不做任何的設(shè)置,Java 應(yīng)用的線程的上下文類加載器默認(rèn)就是系統(tǒng)上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實(shí)現(xiàn)的類。線程上下文類加載器在很多 SPI 的實(shí)現(xiàn)中都會(huì)用到。
下面介紹另外一種加載類的方法:Class.forName
。
Class.forName
Class.forName
是一個(gè)靜態(tài)方法,同樣可以用來(lái)加載類。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)
和 Class.forName(String className)
。第一種形式的參數(shù) name
表示的是類的全名;initialize
表示是否初始化類;loader
表示加載時(shí)使用的類加載器。第二種形式則相當(dāng)于設(shè)置了參數(shù) initialize
的值為 true
,loader
的值為當(dāng)前類的類加載器。Class.forName
的一個(gè)很常見(jiàn)的用法是在加載數(shù)據(jù)庫(kù)驅(qū)動(dòng)的時(shí)候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用來(lái)加載 Apache Derby 數(shù)據(jù)庫(kù)的驅(qū)動(dòng)。
在介紹完類加載器相關(guān)的基本概念之后,下面介紹如何開(kāi)發(fā)自己的類加載器。
回頁(yè)首
開(kāi)發(fā)自己的類加載器
雖然在絕大多數(shù)情況下,系統(tǒng)默認(rèn)提供的類加載器實(shí)現(xiàn)已經(jīng)可以滿足需求。但是在某些情況下,您還是需要為應(yīng)用開(kāi)發(fā)出自己的類加載器。比如您的應(yīng)用通過(guò)網(wǎng)絡(luò)來(lái)傳輸 Java 類的字節(jié)代碼,為了保證安全性,這些字節(jié)代碼經(jīng)過(guò)了加密處理。這個(gè)時(shí)候您就需要自己的類加載器來(lái)從某個(gè)網(wǎng)絡(luò)地址上讀取加密后的字節(jié)代碼,接著進(jìn)行解密和驗(yàn)證,最后定義出要在 Java 虛擬機(jī)中運(yùn)行的類來(lái)。下面將通過(guò)兩個(gè)具體的實(shí)例來(lái)說(shuō)明類加載器的開(kāi)發(fā)。
文件系統(tǒng)類加載器
第一個(gè)類加載器用來(lái)加載存儲(chǔ)在文件系統(tǒng)上的 Java 字節(jié)代碼。完整的實(shí)現(xiàn)如 代碼清單 6 所示。
清單 6. 文件系統(tǒng)類加載器
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
。在 表 1 中列出的 java.lang.ClassLoader
類的常用方法中,一般來(lái)說(shuō),自己開(kāi)發(fā)的類加載器只需要覆寫(xiě) findClass(String name)
方法即可。java.lang.ClassLoader
類的方法 loadClass()
封裝了前面提到的代理模式的實(shí)現(xiàn)。該方法會(huì)首先調(diào)用 findLoadedClass()
方法來(lái)檢查該類是否已經(jīng)被加載過(guò);如果沒(méi)有加載過(guò)的話,會(huì)調(diào)用父類加載器的 loadClass()
方法來(lái)嘗試加載該類;如果父類加載器無(wú)法加載該類的話,就調(diào)用 findClass()
方法來(lái)查找該類。因此,為了保證類加載器都正確實(shí)現(xiàn)代理模式,在開(kāi)發(fā)自己的類加載器時(shí),最好不要覆寫(xiě) loadClass()
方法,而是覆寫(xiě) findClass()
方法。
類 FileSystemClassLoader
的 findClass()
方法首先根據(jù)類的全名在硬盤(pán)上查找類的字節(jié)代碼文件(.class 文件),然后讀取該文件內(nèi)容,最后通過(guò) defineClass()
方法來(lái)把這些字節(jié)代碼轉(zhuǎn)換成 java.lang.Class
類的實(shí)例。
網(wǎng)絡(luò)類加載器
下面將通過(guò)一個(gè)網(wǎng)絡(luò)類加載器來(lái)說(shuō)明如何通過(guò)類加載器來(lái)實(shí)現(xiàn)組件的動(dòng)態(tài)更新。即基本的場(chǎng)景是:Java 字節(jié)代碼(.class)文件存放在服務(wù)器上,客戶端通過(guò)網(wǎng)絡(luò)的方式獲取字節(jié)代碼并執(zhí)行。當(dāng)有版本更新的時(shí)候,只需要替換掉服務(wù)器上保存的文件即可。通過(guò)類加載器可以比較簡(jiǎn)單的實(shí)現(xiàn)這種需求。
類 NetworkClassLoader
負(fù)責(zé)通過(guò)網(wǎng)絡(luò)下載 Java 類字節(jié)代碼并定義出 Java 類。它的實(shí)現(xiàn)與 FileSystemClassLoader
類似。在通過(guò) NetworkClassLoader
加載了某個(gè)版本的類之后,一般有兩種做法來(lái)使用它。第一種做法是使用 Java 反射 API。另外一種做法是使用接口。需要注意的是,并不能直接在客戶端代碼中引用從服務(wù)器上下載的類,因?yàn)榭蛻舳舜a的類加載器找不到這些類。使用 Java 反射 API 可以直接調(diào)用 Java 類的方法。而使用接口的做法則是把接口的類放在客戶端中,從服務(wù)器上加載實(shí)現(xiàn)此接口的不同版本的類。在客戶端通過(guò)相同的接口來(lái)使用這些實(shí)現(xiàn)類。網(wǎng)絡(luò)類加載器的具體代碼見(jiàn) 下載。
在介紹完如何開(kāi)發(fā)自己的類加載器之后,下面說(shuō)明類加載器和 Web 容器的關(guān)系。
類加載器與 Web 容器
對(duì)于運(yùn)行在 Java EE™ 容器中的 Web 應(yīng)用來(lái)說(shuō),類加載器的實(shí)現(xiàn)方式與一般的 Java 應(yīng)用有所不同。不同的 Web 容器的實(shí)現(xiàn)方式也會(huì)有所不同。以 Apache Tomcat 來(lái)說(shuō),每個(gè) Web 應(yīng)用都有一個(gè)對(duì)應(yīng)的類加載器實(shí)例。該類加載器也使用代理模式,所不同的是它是首先嘗試去加載某個(gè)類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規(guī)范中的推薦做法,其目的是使得 Web 應(yīng)用自己的類的優(yōu)先級(jí)高于 Web 容器提供的類。這種代理模式的一個(gè)例外是:Java 核心庫(kù)的類是不在查找范圍之內(nèi)的。這也是為了保證 Java 核心庫(kù)的類型安全。
絕大多數(shù)情況下,Web 應(yīng)用的開(kāi)發(fā)人員不需要考慮與類加載器相關(guān)的細(xì)節(jié)。下面給出幾條簡(jiǎn)單的原則:
- 每個(gè) Web 應(yīng)用自己的 Java 類文件和使用的庫(kù)的 jar 包,分別放在
WEB-INF/classes
和 WEB-INF/lib
目錄下面。
- 多個(gè)應(yīng)用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應(yīng)用共享的目錄下面。
- 當(dāng)出現(xiàn)找不到類的錯(cuò)誤時(shí),檢查當(dāng)前類的類加載器和當(dāng)前線程的上下文類加載器是否正確。
在介紹完類加載器與 Web 容器的關(guān)系之后,下面介紹它與 OSGi 的關(guān)系。
類加載器與 OSGi
OSGi™ 是 Java 上的動(dòng)態(tài)模塊系統(tǒng)。它為開(kāi)發(fā)人員提供了面向服務(wù)和基于組件的運(yùn)行環(huán)境,并提供標(biāo)準(zhǔn)的方式用來(lái)管理軟件的生命周期。OSGi 已經(jīng)被實(shí)現(xiàn)和部署在很多產(chǎn)品上,在開(kāi)源社區(qū)也得到了廣泛的支持。Eclipse 就是基于 OSGi 技術(shù)來(lái)構(gòu)建的。
OSGi 中的每個(gè)模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導(dǎo)入(import)的其它模塊的 Java 包和類(通過(guò) Import-Package
),也可以聲明導(dǎo)出(export)自己的包和類,供其它模塊使用(通過(guò) Export-Package
)。也就是說(shuō)需要能夠隱藏和共享一個(gè)模塊中的某些 Java 包和類。這是通過(guò) OSGi 特有的類加載器機(jī)制來(lái)實(shí)現(xiàn)的。OSGi 中的每個(gè)模塊都有對(duì)應(yīng)的一個(gè)類加載器。它負(fù)責(zé)加載模塊自己包含的 Java 包和類。當(dāng)它需要加載 Java 核心庫(kù)的類時(shí)(以 java
開(kāi)頭的包和類),它會(huì)代理給父類加載器(通常是啟動(dòng)類加載器)來(lái)完成。當(dāng)它需要加載所導(dǎo)入的 Java 類時(shí),它會(huì)代理給導(dǎo)出此 Java 類的模塊來(lái)完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來(lái)加載。只需要設(shè)置系統(tǒng)屬性 org.osgi.framework.bootdelegation
的值即可。
假設(shè)有兩個(gè)模塊 bundleA 和 bundleB,它們都有自己對(duì)應(yīng)的類加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類 com.bundleA.Sample
,并且該類被聲明為導(dǎo)出的,也就是說(shuō)可以被其它模塊所使用的。bundleB 聲明了導(dǎo)入 bundleA 提供的類 com.bundleA.Sample
,并包含一個(gè)類 com.bundleB.NewSample
繼承自 com.bundleA.Sample
。在 bundleB 啟動(dòng)的時(shí)候,其類加載器 classLoaderB 需要加載類 com.bundleB.NewSample
,進(jìn)而需要加載類 com.bundleA.Sample
。由于 bundleB 聲明了類 com.bundleA.Sample
是導(dǎo)入的,classLoaderB 把加載類 com.bundleA.Sample
的工作代理給導(dǎo)出該類的 bundleA 的類加載器 classLoaderA。classLoaderA 在其模塊內(nèi)部查找類 com.bundleA.Sample
并定義它,所得到的類 com.bundleA.Sample
實(shí)例就可以被所有聲明導(dǎo)入了此類的模塊使用。對(duì)于以 java
開(kāi)頭的類,都是由父類加載器來(lái)加載的。如果聲明了系統(tǒng)屬性 org.osgi.framework.bootdelegation=com.example.core.*
,那么對(duì)于包 com.example.core
中的類,都是由父類加載器來(lái)完成的。
OSGi 模塊的這種類加載器結(jié)構(gòu),使得一個(gè)類的不同版本可以共存在 Java 虛擬機(jī)中,帶來(lái)了很大的靈活性。不過(guò)它的這種不同,也會(huì)給開(kāi)發(fā)人員帶來(lái)一些麻煩,尤其當(dāng)模塊需要使用第三方提供的庫(kù)的時(shí)候。下面提供幾條比較好的建議:
- 如果一個(gè)類庫(kù)只有一個(gè)模塊使用,把該類庫(kù)的 jar 包放在模塊中,在
Bundle-ClassPath
中指明即可。
- 如果一個(gè)類庫(kù)被多個(gè)模塊共用,可以為這個(gè)類庫(kù)單獨(dú)的創(chuàng)建一個(gè)模塊,把其它模塊需要用到的 Java 包聲明為導(dǎo)出的。其它模塊聲明導(dǎo)入這些類。
- 如果類庫(kù)提供了 SPI 接口,并且利用線程上下文類加載器來(lái)加載 SPI 實(shí)現(xiàn)的 Java 類,有可能會(huì)找不到 Java 類。如果出現(xiàn)了
NoClassDefFoundError
異常,首先檢查當(dāng)前線程的上下文類加載器是否正確。通過(guò) Thread.currentThread().getContextClassLoader()
就可以得到該類加載器。該類加載器應(yīng)該是該模塊對(duì)應(yīng)的類加載器。如果不是的話,可以首先通過(guò) class.getClassLoader()
來(lái)得到模塊對(duì)應(yīng)的類加載器,再通過(guò) Thread.currentThread().setContextClassLoader()
來(lái)設(shè)置當(dāng)前線程的上下文類加載器。
總結(jié)
類加載器是 Java 語(yǔ)言的一個(gè)創(chuàng)新。它使得動(dòng)態(tài)安裝和更新軟件組件成為可能。本文詳細(xì)介紹了類加載器的相關(guān)話題,包括基本概念、代理模式、線程上下文類加載器、與 Web 容器和 OSGi 的關(guān)系等。開(kāi)發(fā)人員在遇到 ClassNotFoundException
和 NoClassDefFoundError
等異常的時(shí)候,應(yīng)該檢查拋出異常的類的類加載器和當(dāng)前線程的上下文類加載器,從中可以發(fā)現(xiàn)問(wèn)題的所在。在開(kāi)發(fā)自己的類加載器的時(shí)候,需要注意與已有的類加載器組織結(jié)構(gòu)的協(xié)調(diào)。
posted on 2010-04-24 22:26
junly 閱讀(601)
評(píng)論(0) 編輯 收藏 所屬分類:
java