類加載是java語言提供的最強(qiáng)大的機(jī)制之一,。盡管類加載并不是討論的熱點(diǎn)話題,但所有的編程人員都應(yīng)該了解其工作機(jī)制,,明白如何
做才能讓其滿足我們的需要。這能有效節(jié)省我們的編碼時(shí)間,,從不斷調(diào)試ClassNotFoundException,
ClassCastException的工作中解脫出來,。 在JAVA中,,一個類用其完全匹配類名(fully qualified class name)作為標(biāo)識,這里指的完全匹配類名包括包名和類名,。但在JVM中一個類用其全名和一個加載類ClassLoader的實(shí)例作為唯一標(biāo)識,。因此,如 果一個名為Pg的包中,,有一個名為Cl的類,,被類加載器KlassLoader的一個實(shí)例kl1加載,Cl的實(shí)例,,即C1.class在JVM中表示為 (Cl, Pg, kl1),。這意味著兩個類加載器的實(shí)例(Cl, Pg, kl1) 和 (Cl, Pg, kl2)是不同的,被它們所加載的類也因此完全不同,,互不兼容的,。那么在JVM中到底有多少種類加載器的實(shí)例?下一節(jié)我們揭示答案,。 類加載器 在JVM中,,每一個類都被java.lang.ClassLoader的一些實(shí)例來加載.類ClassLoader是在包中java.lang里,開發(fā)者可以自由地繼承它并添加自己的功能來加載類,。 無論何時(shí)我們鍵入java MyMainClass來開始運(yùn)行一個新的JVM,,“引導(dǎo)類加載器(bootstrap class loader)”負(fù)責(zé)將一些關(guān)鍵的Java類,如java.lang.Object和其他一些運(yùn)行時(shí)代碼先加載進(jìn)內(nèi)存中,。運(yùn)行時(shí)的類在JRE\lib \rt.jar包文件中,。因?yàn)檫@屬于系統(tǒng)底層執(zhí)行動作,我們無法在JAVA文檔中找到引導(dǎo)類加載器的工作細(xì)節(jié),?;谕瑯拥脑颍龑?dǎo)類加載器的行為在各 JVM之間也是大相徑庭,。 同理,,如果我們按照如下方式: log(java.lang.String.class.getClassLoader()); 來獲取java的核心運(yùn)行時(shí)類的加載器,就會得到null,。 接下來介紹java的擴(kuò)展類加載器,。擴(kuò)展庫提供比java運(yùn)行代碼更多的特性,,我們可以把擴(kuò)展庫保存在由java.ext.dirs屬性提供的路徑中。 (編輯注:java.ext.dirs屬性指的是系統(tǒng)屬性下的一個key,,所有的系統(tǒng)屬性可以通過System.getProperties()方法獲 得,。在編者的系統(tǒng)中,java.ext.dirs的value是” C:\Program Files\Java\jdk1.5.0_04\jre\lib\ext”,。下面將要談到的如java.class.path也同屬系統(tǒng)屬性的一個 key,。) 類ExtClassLoader專門用來加載所有java.ext.dirs下的.jar文件。開發(fā)者可以通過把自己的.jar文件或庫文件加入到擴(kuò)展目錄的classpath,,使其可以被擴(kuò)展類加載器讀取。 從開發(fā)者的角度,,第三種同樣也是最重要的一種類加載器是AppClassLoader,。這種類加載器用來讀取所有的對應(yīng)在java.class.path系統(tǒng)屬性的路徑下的類。 Sun的java指南中,,文章“理解擴(kuò)展類加載”(Understanding Extension Class Loading)對以上三個類加載器路徑有更詳盡的解釋,,這是其他幾個JDK中的類加載器 ●java.net.URLClassLoader ●java.security.SecureClassLoader ●java.rmi.server.RMIClassLoader ●sun.applet.AppletClassLoader java.lang.Thread,包含了public ClassLoader getContextClassLoader()方法,,這一方法返回針對一具體線程的上下文環(huán)境類加載器,。此類加載器由線程的創(chuàng)建者提供,以供此線程中運(yùn) 行的代碼在需要加載類或資源時(shí)使用,。如果此加載器未被建立,,缺省是其父線程的上下文類加載器。原始的類加載器一般由讀取應(yīng)用程序的類加載器建立,。 類加載器如何工作,? 除了引導(dǎo)類加載器,所有的類加載器都有一個父類加載器,,不僅如此,,所有的類加載器也都是java.lang.ClassLoader類型。以上兩種類加載 器是不同的,,而且對于開發(fā)者自訂制的類加載器的正常運(yùn)行也至關(guān)重要,。最重要的方面是正確設(shè)置父類加載器。任何類加載器,,其父類加載器是加載該類加載器的類 加載器實(shí)例,。(記住,類加載器本身也是一個類?。?br> 使用loadClass()方法可以從類加載器中獲得該類,。我們可以通過java.lang.ClassLoader的源代碼來了解該方法工作的細(xì)節(jié),如下: protected synchronized Class<?> loadClass (String name, boolean resolve) throws ClassNotFoundException{ // First check if the class is already loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // If still not found, then invoke // findClass to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } 我們可以使用ClassLoader的兩種構(gòu)造方法來設(shè)置父類加載器: public class MyClassLoader extends ClassLoader{ public MyClassLoader(){ super(MyClassLoader.class.getClassLoader()); } } 或 public class MyClassLoader extends ClassLoader{ public MyClassLoader(){ super(getClass().getClassLoader()); } } 第一種方式較為常用,,因?yàn)橥ǔ2唤ㄗh在構(gòu)造方法里調(diào)用getClass()方法,,因?yàn)閷ο蟮某跏蓟皇窃跇?gòu)造方法的出口處才完全完成,。因此,如果父類加載 器被正確建立,,當(dāng)要示從一個類加載器的實(shí)例獲得一個類時(shí),,如果它不能找到這個類,它應(yīng)該首先去訪問其父類,。如果父類不能找到它(即其父類也不能找不這個 類,,等等),,而且如果findBootstrapClass0()方法也失敗了,則調(diào)用findClass()方法,。findClass()方法的缺省實(shí) 現(xiàn)會拋出ClassNotFoundException,當(dāng)它們繼承java.lang.ClassLoader來訂制類加載器時(shí)開發(fā)者需要實(shí)現(xiàn)這個方 法,。findClass()的缺省實(shí)現(xiàn)方式如下: protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } 在findClass()方法內(nèi)部,,類加載器需要獲取任意來源的字節(jié)碼。來源可以是文件系統(tǒng),,URL,數(shù)據(jù)庫,,可以產(chǎn)生字節(jié)碼的另一個應(yīng)用程序,及其他類 似的可以產(chǎn)生java規(guī)范的字節(jié)碼的來源,。你甚至可以使用BCEL (Byte Code Engineering Library:字節(jié)碼工程庫),它提供了運(yùn)行時(shí)創(chuàng)建類的捷徑,。BCEL已經(jīng)被成功地使用在以下方面:編譯器,,優(yōu)化器,混淆器,,代碼產(chǎn)生器及其他分析工 具。一旦字節(jié)碼被檢索,,此方法就會調(diào)用defineClass()方法,,此行為對不同的類加載實(shí)例是有差異的。因此,,如果兩個類加載實(shí)例從同一個來源定義 一個類,,所定義的結(jié)果是不同的。 JAVA語言規(guī)范(Java language specification)詳細(xì)解釋了JAVA執(zhí)行引擎中的類或接口的加載(loading),,鏈接(linking)或初始化(initialization)過程,。 圖一顯示了一個主類稱為MyMainClass的應(yīng)用程序,。依照之前的闡述,,MyMainClass.class會被AppClassLoader加載。 MyMainClass創(chuàng)建了兩個類加載器的實(shí)例:CustomClassLoader1 和 CustomClassLoader2,他們可以從某數(shù)據(jù)源(比如網(wǎng)絡(luò))獲取名為Target的字節(jié)碼,。這表示類Target的類定義不在應(yīng)用程序類路徑 或擴(kuò)展類路徑,。在這種情況下,如果MyMainClass想要用自定義的類加載器加載Target類,,CustomClassLoader1和 CustomClassLoader2會分別獨(dú)立地加載并定義Target.class類,。這在java中有重要的意義,。如果Target類有一些靜態(tài)的 初始化代碼,并且假設(shè)我們只希望這些代碼在JVM中只執(zhí)行一次,,而這些代碼在我們目前的步驟中會執(zhí)行兩次——分別被不同的 CustomClassLoaders加載并執(zhí)行。如果類Target被兩個CustomClassLoaders加載并創(chuàng)建兩個實(shí)例Target1和 Target2,,如圖一顯示,,它們不是類型兼容的。換句話說,,在JVM中無法執(zhí)行以下代碼: Target target3 = (Target) target2; 以上代碼會拋出一個ClassCastException,。這是因?yàn)镴VM把他們視為分別不同的類,因?yàn)樗麄儽徊煌念惣虞d器所定義,。這種情況當(dāng)我們不是 使用兩個不同的類加載器CustomClassLoader1 和 CustomClassLoader2,,而是使用同一個類加載器CustomClassLoader的不同實(shí)例時(shí),,也會出現(xiàn)同樣的錯誤,。這些會在本文后邊 用具體代碼說明,。 圖1. 在同一個JVM中多個類加載器加載同一個目標(biāo)類 關(guān)于類加載,、定義和鏈接的更多解釋,,請參考Andreas Schaefer的"Inside Class Loaders." 為什么我們需要我們自己的類加載器 原因之一為開發(fā)者寫自己的類加載器來控制JVM中的類加載行為,,java中的類靠其包名和類名來標(biāo)識,,對于實(shí)現(xiàn)了 java.io.Serializable接口的類,,serialVersionUID扮演了一個標(biāo)識類版本的重要角色,。這個唯一標(biāo)識是一個類名,、接口 名,、成員方法及屬性等組成的一個64位的哈希字段,,而且也沒有其他快捷的方式來標(biāo)識一個類的版本,。嚴(yán)格說來,,如果以上的都匹配,,那么則屬于同一個類。 但是讓我們思考如下情況:我們需要開發(fā)一個通用的執(zhí)行引擎,??梢詧?zhí)行實(shí)現(xiàn)某一特定接口的任何任務(wù),。當(dāng)任務(wù)被提交到這個引擎,,首先需要加載這個任務(wù)的代碼,。 假設(shè)不同的客戶對此引擎提交了不同的任務(wù),,湊巧,,這些所有的任務(wù)都有一個相同的類名和包名。現(xiàn)在面臨的問題就是這個引擎是否可以針對不同的用戶所提交的信 息而做出不同的反應(yīng),。這一情況在下文的參考一節(jié)有可供下載的代碼樣例,,samepath 和 differentversions,,這兩個目錄分別演示了這一概念,。 圖2 顯示了文件目錄結(jié)構(gòu),,有三個子目錄samepath, differentversions, 和 differentversionspush,,里邊是例子: 圖2. 文件夾結(jié)構(gòu)組織示例 在samepath 中,,類version.Version保存在v1和v2兩個子目錄里,,兩個類具有同樣的類名和包名,,唯一不同的是下邊這行: public void fx(){ log("this = " + this + "; Version.fx(1)."); } V1中,,日志記錄中有Version.fx(1),,而在v2中則是Version.fx(2)。把這個兩個存在細(xì)微不同的類放在一個classpath下,,然后運(yùn)行Test類: set CLASSPATH=.;%CURRENT_ROOT%\v1;%CURRENT_ROOT%\v2 %JAVA_HOME%\bin\java Test 圖3顯示了控制臺輸出,。我們可以看到對應(yīng)著Version.fx(1)的代碼被執(zhí)行了,,因?yàn)轭惣虞d器在classpath首先看到此版本的代碼,。 圖3. 在類路徑中samepath測試排在最前面的version 1 再次運(yùn)行,類路徑做如下微小改動,。 set CLASSPATH=.;%CURRENT_ROOT%\v2;%CURRENT_ROOT%\v1 %JAVA_HOME%\bin\java Test 控制臺的輸出變?yōu)閳D4。對應(yīng)著Version.fx(2)的代碼被加載,,因?yàn)轭惣虞d器在classpath中首先找到它的路徑。 圖4. 在類路徑中samepath測試排在最前面的version 2 根據(jù)以上例子可以很明顯地看出,,類加載器加載在類路徑中被首先找到的元素,。如果我們在v1和v2中刪除了version.Version,,做一個非 version.Version形式的.jar文件,,如myextension.jar,,把它放到對應(yīng)java.ext.dirs的路徑下,,再次執(zhí)行后看 到version.Version不再被AppClassLoader加載,,而是被擴(kuò)展類加載器加載,。如圖5所示,。 圖5. AppClassLoader及ExtClassLoader 繼續(xù)這個例子,文件夾differentversions包含了一個RMI執(zhí)行引擎,,客戶端可以提供給執(zhí)行引擎任何實(shí)現(xiàn)了common.TaskIntf 接口的任務(wù),。子文件夾client1 和 client2包含了類client.TaskImpl有個細(xì)微不同的兩個版本,。兩個類的區(qū)別在以下幾行: static{ log("client.TaskImpl.class.getClassLoader (v1) : " + TaskImpl.class.getClassLoader()); } public void execute(){ log("this = " + this + "; execute(1)"); } 在client1和client2里分別有g(shù)etClassLoader(v1) 與 execute(1)和getClassLoader(v2) 與 execute(2)的的log語句。并且,在開始執(zhí)行引擎RMI服務(wù)器的代碼中,,我們隨意地將client2的任務(wù)實(shí)現(xiàn)放在類路徑的前面,。 CLASSPATH=%CURRENT_ROOT%\common;%CURRENT_ROOT%\server; %CURRENT_ROOT%\client2;%CURRENT_ROOT%\client1 %JAVA_HOME%\bin\java server.Server 如圖6,,7,,8的屏幕截圖,,在客戶端VM,,各自的client.TaskImpl類被加載、實(shí)例化,,并發(fā)送到服務(wù)端的VM來執(zhí)行。從服務(wù)端的控制臺,,可以 明顯看到client.TaskImpl代碼只被服務(wù)端的VM執(zhí)行一次,,這個單一的代碼版本在服務(wù)端多次生成了許多實(shí)例,,并執(zhí)行任務(wù),。 圖6. 執(zhí)行引擎服務(wù)器控制臺 圖6顯示了服務(wù)端的控制臺,,加載并執(zhí)行兩個不同的客戶端的請求,,如圖7,,8所示,。需要注意的是,代碼只被加載了一次(從靜態(tài)初始化塊的日志中也可以明顯看出),,但對于客戶端的調(diào)用這個方法被執(zhí)行了兩次。 圖7. 執(zhí)行引擎客戶端 1控制臺 圖7中,,客戶端VM加載了含有client.TaskImpl.class.getClassLoader(v1)的日志內(nèi)容的類TaskImpl的代碼,,并提供給服務(wù)端的執(zhí)行引擎。圖8的客戶端VM加載了另一個TaskImpl的代碼,,并發(fā)送給服務(wù)端,。 圖8. 執(zhí)行引擎客戶端 2控制臺 在客戶端的VM中,類client.TaskImpl被分別加載,,初始化,,并發(fā)送到服務(wù)端執(zhí)行。圖6還揭示了client.TaskImpl的代碼只在服 務(wù)端的VM中加載了一次,,但這“唯一的一次”卻在服務(wù)端創(chuàng)造了許多實(shí)例并執(zhí)行,?;蛟S客戶端1該不高興了因?yàn)椴⒉皇撬? client.TaskImpl(v1)的方法調(diào)用被服務(wù)端執(zhí)行了,,而是其他的一些代碼,。如何解決這一問題,?答案就是實(shí)現(xiàn)定制的類加載器,。 定制類加載器 要較好地控制類的加載,就要實(shí)現(xiàn)定制的類加載器,。所有自定義的類加載器都應(yīng)繼承自java.lang.ClassLoader,。而且在構(gòu)造方法中,我們也 應(yīng)該設(shè)置父類加載器,。然后重寫findClass()方法,。differentversionspush文件夾包含了一個叫做 FileSystemClassLoader的自訂制的類加載器,。其結(jié)構(gòu)如圖9所示。 圖9. 定制類加載器關(guān)系 以下是在common.FileSystemClassLoader實(shí)現(xiàn)的主方法: public byte[] findClassBytes(String className){ try{ String pathName = currentRoot + File.separatorChar + className. replace('.', File.separatorChar) + ".class"; FileInputStream inFile = new FileInputStream(pathName); byte[] classBytes = new byte[inFile.available()]; inFile.read(classBytes); return classBytes; } catch (java.io.IOException ioEx){ return null; } } public Class findClass(String name)throws ClassNotFoundException{ byte[] classBytes = findClassBytes(name); if (classBytes==null){ throw new ClassNotFoundException(); } else{ return defineClass(name, classBytes, 0, classBytes.length); } } public Class findClass(String name, byte[] classBytes)throws ClassNotFoundException{ if (classBytes==null){ throw new ClassNotFoundException( "(classBytes==null)"); } else{ return defineClass(name, classBytes, 0, classBytes.length); } } public void execute(String codeName, byte[] code){ Class klass = null; try{ klass = findClass(codeName, code); TaskIntf task = (TaskIntf) klass.newInstance(); task.execute(); } catch(Exception exception){ exception.printStackTrace(); } } 這個類供客戶端把client.TaskImpl(v1)轉(zhuǎn)換成字節(jié)數(shù)組,,之后此字節(jié)數(shù)組被發(fā)送到RMI服務(wù)端,。在服務(wù)端,一個同樣的類用來把字節(jié)數(shù)組的內(nèi)容轉(zhuǎn)換回代碼,??蛻舳舜a如下: public class Client{ public static void main (String[] args){ try{ byte[] code = getClassDefinition ("client.TaskImpl"); serverIntf.execute("client.TaskImpl", code); } catch(RemoteException remoteException){ remoteException.printStackTrace(); } } private static byte[] getClassDefinition (String codeName){ String userDir = System.getProperties(). getProperty("BytePath"); FileSystemClassLoader fscl1 = null; try{ fscl1 = new FileSystemClassLoader (userDir); } catch(FileNotFoundException fileNotFoundException){ fileNotFoundException.printStackTrace(); } return fscl1.findClassBytes(codeName); } } 在執(zhí)行引擎中,,從客戶端收到的代碼被送到定制的類加載器中,。定制的類加載器把其從字節(jié)數(shù)組定義成類,實(shí)例化并執(zhí)行,。需要指出的是,,對每一個客戶請求,我們 用類FileSystemClassLoader的不同實(shí)例來定義客戶端提交的client.TaskImpl,。而且,,client.TaskImpl并 不在服務(wù)端的類路徑中。這也就意味著當(dāng)我們在FileSystemClassLoader調(diào)用findClass()方法時(shí),,findClass()調(diào)用 內(nèi)在的defineClass()方法,。類client.TaskImpl被特定的類加載器實(shí)例所定義。因此,,當(dāng) FileSystemClassLoader的一個新的實(shí)例被使用,類又被重新定義為字節(jié)數(shù)組,。因此,對每個客戶端請求類client.TaskImpl 被多次定義,,我們就可以在相同執(zhí)行引擎JVM中執(zhí)行不同的client.TaskImpl的代碼,。 public void execute(String codeName, byte[] code)throws RemoteException{ FileSystemClassLoader fileSystemClassLoader = null; try{ fileSystemClassLoader = new FileSystemClassLoader(); fileSystemClassLoader.execute(codeName, code); } catch(Exception exception){ throw new RemoteException(exception.getMessage()); } }
|
|