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