類加載器是 Java 語(yǔ)言的一個(gè)創(chuàng)新,也是 Java 語(yǔ)言流行的重要原因之一。它使得 Java 類可以被動(dòng)態(tài)加載到 Java
虛擬機(jī)中并執(zhí)行。類加載器從 JDK 1.0 就出現(xiàn)了,最初是為了滿足 Java Applet 的需要而開發(fā)出來(lái)的。Java Applet
需要從遠(yuǎn)程下載 Java 類文件到瀏覽器中并執(zhí)行。現(xiàn)在類加載器在 Web 容器和 OSGi 中得到了廣泛的使用。一般來(lái)說(shuō),Java
應(yīng)用的開發(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ō)明。下面介紹類加載器的樹狀組織結(jié)構(gòu)。
類加載器的樹狀組織結(jié)構(gòu)
Java 中的類加載器大致可以分成兩類,一類是系統(tǒng)提供的,另外一類則是由 Java
應(yīng)用開發(fā)人員編寫的。系統(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)提供的類加載器以外,開發(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ì)于開發(fā)人員編寫的類
加載器來(lái)說(shuō),其父類加載器是加載此類加載器 Java 類的類加載器。因?yàn)轭惣虞d器 Java 類如同其它的 Java
類一樣,也是要由類加載器來(lái)加載的。一般來(lái)說(shuō),開發(fā)人員編寫的類加載器的父類加載器是系統(tǒng)類加載器。類加載器通過(guò)這種方式組織起來(lái),形成樹狀結(jié)構(gòu)。樹的根
節(jié)點(diǎn)就是引導(dǎo)類加載器。圖
1 中給出了一個(gè)典型的類加載器樹狀組織結(jié)構(gòu)示意圖,其中的箭頭指向的是父類加載器。
圖 1. 類加載器樹狀組織結(jié)構(gòu)示意圖
代
碼清單 1 演示了類加載器的樹狀組織結(jié)構(gòu)。
清單 1. 演示類加載器的樹狀組織結(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. 演示類加載器的樹狀組織結(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í)例。需要注意的是這里并沒有輸出引導(dǎo)類加載器,這是由于有些 JDK 的實(shí)現(xiàn)對(duì)于父類加載器是引導(dǎo)類加載器的情況,getParent()
方法返回 null 。
在了解了類加載器的樹狀組織結(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 開始引入的。類 java.lang.Thread
中的方法 getContextClassLoader() 和 setContextClassLoader(ClassLoader
cl) 用來(lái)獲取和設(shè)置線程的上下文類加載器。如果沒有通過(guò) setContextClassLoader(ClassLoader
cl) 方法進(jìn)行設(shè)置的話,線程將繼承其父線程的上下文類加載器。Java
應(yīng)用運(yùn)行的初始線程的上下文類加載器是系統(tǒng)類加載器。在線程中運(yùn)行的代碼可以通過(guò)此類加載器來(lái)加載類和資源。
前面提到的類加載器的代理模式并不能解決 Java 應(yīng)用開發(fā)中會(huì)遇到的類加載器的全部問(wèn)題。Java
提供了很多服務(wù)提供者接口(Service Provider Interface,SPI),允許第三方為這些接口提供實(shí)現(xià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è)很常見的用法是在加載數(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)的基本概念之后,下面介紹如何開發(fā)自己的類加載器。
回頁(yè)首
開發(fā)自己的類加載器
雖然在絕大多數(shù)情況下,系統(tǒng)默認(rèn)提供的類加載器實(shí)現(xiàn)已經(jīng)可以滿足需求。但是在某些情況下,您還是需要為應(yīng)用開發(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ō)明類加載器的開發(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ō),自己開發(fā)的類加載器只需要覆寫 findClass(String name) 方法即可。java.lang.ClassLoader
類的方法 loadClass() 封裝了前面提到的代理模式的實(shí)現(xiàn)。該方法會(huì)首先調(diào)用 findLoadedClass()
方法來(lái)檢查該類是否已經(jīng)被加載過(guò);如果沒有加載過(guò)的話,會(huì)調(diào)用父類加載器的 loadClass()
方法來(lái)嘗試加載該類;如果父類加載器無(wú)法加載該類的話,就調(diào)用 findClass()
方法來(lái)查找該類。因此,為了保證類加載器都正確實(shí)現(xiàn)代理模式,在開發(fā)自己的類加載器時(shí),最好不要覆寫 loadClass()
方法,而是覆寫 findClass() 方法。
類 FileSystemClassLoader 的 findClass()
方法首先根據(jù)類的全名在硬盤上查找類的字節(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ò)類加載
器的具體代碼見 下
載。
在介紹完如何開發(fā)自己的類加載器之后,下面說(shuō)明類加載器和 Web 容器的關(guān)系。
回頁(yè)首
類加載器與 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)用的開發(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)系。
回頁(yè)首
類加載器與 OSGi
OSGi™ 是 Java
上的動(dòng)態(tài)模塊系統(tǒng)。它為開發(fā)人員提供了面向服務(wù)和基于組件的運(yùn)行環(huán)境,并提供標(biāo)準(zhǔn)的方式用來(lái)管理軟件的生命周期。OSGi
已經(jīng)被實(shí)現(xiàn)和部署在很多產(chǎn)品上,在開源社區(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
開頭的包和類),它會(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
開頭的類,都是由父類加載器來(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ì)給開發(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)前線程的上下文類加載器。
回頁(yè)首
總結(jié)
類加載器是 Java
語(yǔ)言的一個(gè)創(chuàng)新。它使得動(dòng)態(tài)安裝和更新軟件組件成為可能。本文詳細(xì)介紹了類加載器的相關(guān)話題,包括基本概念、代理模式、線程上下文類加載器、與 Web
容器和 OSGi 的關(guān)系等。開發(fā)人員在遇到 ClassNotFoundException 和 NoClassDefFoundError
等異常的時(shí)候,應(yīng)該檢查拋出異常的類的類加載器和當(dāng)前線程的上下文類加載器,從中可以發(fā)現(xiàn)問(wèn)題的所在。在開發(fā)自己的類加載器的時(shí)候,需要注意與已有的類加
載器組織結(jié)構(gòu)的協(xié)調(diào)。
|