原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-classloader/?ca=drs-tp4608java
類加載器是 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 字節代碼多是經過工具動態生成的,也多是經過網絡下載的。bootstrap
基本上全部的類加載器都是 java.lang.ClassLoader
類的一個實例。下面詳細介紹這個 Java 類。數組
java.lang.ClassLoader
類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,而後從這些字節代碼中定義出一個 Java 類,即 java.lang.Class
類的一個實例。除此以外,ClassLoader
還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。不過本文只討論其加載類的功能。爲了完成加載類的這個職責,ClassLoader
提供了一系列的方法,比較重要的方法如 表 1所示。關於這些方法的細節會在下面進行介紹。緩存
方法 | 說明 |
---|---|
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中給出的方法,表示類名稱的 name
參數的值是類的二進制名稱。須要注意的是內部類的表示,如 com.example.Sample$1
和 com.example.Sample$Inner
等表示方式。這些方法會在下面介紹類加載器的工做機制時,作進一步的說明。下面介紹類加載器的樹狀組織結構。服務器
Java 中的類加載器大體能夠分紅兩類,一類是系統提供的,另一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:
java.lang.ClassLoader
。ClassLoader.getSystemClassLoader()
來獲取它。除了系統提供的類加載器之外,開發人員能夠經過繼承 java.lang.ClassLoader
類的方式實現本身的類加載器,以知足一些特殊的需求。
除了引導類加載器以外,全部的類加載器都有一個父類加載器。經過 表 1中給出的 getParent()
方 法能夠獲得。對於系統提供的類加載器來講,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對於開發人員編寫的類加 載器來講,其父類加載器是加載此類加載器 Java 類的類加載器。由於類加載器 Java 類如同其它的 Java 類同樣,也是要由類加載器來加載的。通常來講,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器經過這種方式組織起來,造成樹狀結構。樹的根 節點就是引導類加載器。圖 1中給出了一個典型的類加載器樹狀組織結構示意圖,其中的箭頭指向的是父類加載器。
代碼清單 1演示了類加載器的樹狀組織結構。
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中經過遞歸調用 getParent()
方法來輸出所有的父類加載器。代碼清單 1的運行結果如 代碼清單 2所示。
sun.misc.Launcher$AppClassLoader@9304b1 sun.misc.Launcher$ExtClassLoader@190d11 |
如 代碼清單 2所示,第一個輸出的是 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中給出了 Java 類 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
類型的參數,而且會把該參數強制轉換成 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
上調用方法 setSample
,試圖把對象 obj2
賦值給 obj1
內部的 instance
對象。代碼清單 4的運行結果如 代碼清單 5所示。
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給出的運行結果能夠看到,運行時拋出了 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所包含的 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(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所示。
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
類的經常使用方法中,通常來講,本身開發的類加載器只須要覆寫 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 類的方法。而使用接口的作法則是把接口的類放在客戶端中,從服務器上加載實現此接口的不一樣版本的類。在客戶端經過相同的接口來使用這些實現類。網絡類加載 器的具體代碼見 下載。
在介紹完如何開發本身的類加載器以後,下面說明類加載器和 Web 容器的關係。
對於運行在 Java EE™容器中的 Web 應用來講,類加載器的實現方式與通常的 Java 應用有所不一樣。不一樣的 Web 容器的實現方式也會有所不一樣。以 Apache Tomcat 來講,每一個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不一樣的是它是首先嚐試去加載某個類,若是找不到再代理給父類加載器。這與通常類加載器的順 序是相反的。這是 Java Servlet 規範中的推薦作法,其目的是使得 Web 應用本身的類的優先級高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找範圍以內的。這也是爲了保證 Java 核心庫的類型安全。
絕大多數狀況下,Web 應用的開發人員不須要考慮與類加載器相關的細節。下面給出幾條簡單的原則:
WEB-INF/classes
和 WEB-INF/lib
目錄下面。在介紹完類加載器與 Web 容器的關係以後,下面介紹它與 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 虛擬機中,帶來了很大的靈活性。不過它的這種不一樣,也會給開發人員帶來一些麻煩,尤爲當模塊須要使用第三方提供的庫的時候。下面提供幾條比較好的建議:
Bundle-ClassPath
中指明便可。NoClassDefFoundError
異常,首先檢查當前線程的上下文類加載器是否正確。經過 Thread.currentThread().getContextClassLoader()
就能夠獲得該類加載器。該類加載器應該是該模塊對應的類加載器。若是不是的話,能夠首先經過 class.getClassLoader()
來獲得模塊對應的類加載器,再經過 Thread.currentThread().setContextClassLoader()
來設置當前線程的上下文類加載器。類加載器是 Java 語言的一個創新。它使得動態安裝和更新軟件組件成爲可能。本文詳細介紹了類加載器的相關話題,包括基本概念、代理模式、線程上下文類加載器、與 Web 容器和 OSGi 的關係等。開發人員在遇到 ClassNotFoundException
和 NoClassDefFoundError
等異常的時候,應該檢查拋出異常的類的類加載器和當前線程的上下文類加載器,從中能夠發現問題的所在。在開發本身的類加載器的時候,須要注意與已有的類加載器組織結構的協調。