【原文地址:https://www6.software.ibm.com/developerworks/cn/education/java/j-classloader/tutorial/】
1.介紹
2.ClassLoader的結(jié)構(gòu)
3.Compiling ClassLoader
4.java2 中ClassLoader的變動(dòng)
5.源代碼
---------------------------------------------------------------------------
第一章 介紹
什么是 ClassLoader
在流行的商業(yè)化編程語言中,Java 語言由于在 Java 虛擬機(jī) (JVM) 上運(yùn)行而顯得與眾不同。這意味著已編譯的程序是一種特殊的、獨(dú)立于平臺(tái)的格式,并非依賴于它們所運(yùn)行的機(jī)器。在很大程度上,這種格式不同于傳統(tǒng)的可執(zhí)行程序格式。
與 C 或 C++ 編寫的程序不同,Java 程序并不是一個(gè)可執(zhí)行文件,而是由許多獨(dú)立的類文件組成,每一個(gè)文件對(duì)應(yīng)于一個(gè) Java 類。
此外,這些類文件并非立即全部都裝入內(nèi)存,而是根據(jù)程序需要裝入內(nèi)存。ClassLoader 是 JVM 中將類裝入內(nèi)存的那部分。
而且,Java ClassLoader 就是用 Java 語言編寫的。這意味著創(chuàng)建您自己的 ClassLoader 非常容易,不必了解 JVM 的微小細(xì)節(jié)。
為什么編寫 ClassLoader?
如果 JVM 已經(jīng)有一個(gè) ClassLoader,那么為什么還要編寫另一個(gè)呢?問得好。缺省的 ClassLoader 只知道如何從本地文件系統(tǒng)裝入類文件。不過這只適合于常規(guī)情況,即已全部編譯完 Java 程序,并且計(jì)算機(jī)處于等待狀態(tài)。
但 Java 語言最具新意的事就是 JVM 可以非常容易地從那些非本地硬盤或從網(wǎng)絡(luò)上獲取類。例如,瀏覽者可以使用定制的 ClassLoader 從 Web 站點(diǎn)裝入可執(zhí)行內(nèi)容。
有許多其它方式可以獲取類文件。除了簡單地從本地或網(wǎng)絡(luò)裝入文件以外,可以使用定制的 ClassLoader 完成以下任務(wù):
- 在執(zhí)行非置信代碼之前,自動(dòng)驗(yàn)證數(shù)字簽名
- 使用用戶提供的密碼透明地解密代碼
- 動(dòng)態(tài)地創(chuàng)建符合用戶特定需要的定制化構(gòu)建類
任何您認(rèn)為可以生成 Java 字節(jié)碼的內(nèi)容都可以集成到應(yīng)用程序中。
定制 ClassLoader 示例
如果使用過 JDK 或任何基于 Java 瀏覽器中的 Applet 查看器,那么您差不多肯定使用過定制的 ClassLoader。
Sun 最初發(fā)布 Java 語言時(shí),其中最令人興奮的一件事是觀看這項(xiàng)新技術(shù)是如何執(zhí)行在運(yùn)行時(shí)從遠(yuǎn)程的 Web 服務(wù)器裝入的代碼。(此外,還有更令人興奮的事 -- Java 技術(shù)提供了一種便于編寫代碼的強(qiáng)大語言。)更一些令人激動(dòng)的是它可以執(zhí)行從遠(yuǎn)程 Web 服務(wù)器通過 HTTP 連接發(fā)送過來的字節(jié)碼。
此項(xiàng)功能歸功于 Java 語言可以安裝定制 ClassLoader。Applet 查看器包含一個(gè) ClassLoader,它不在本地文件系統(tǒng)中尋找類,而是訪問遠(yuǎn)程服務(wù)器上的 Web 站點(diǎn),經(jīng)過 HTTP 裝入原始的字節(jié)碼文件,并把它們轉(zhuǎn)換成 JVM 內(nèi)的類。
瀏覽器和 Applet 查看器中的 ClassLoaders 還可以做其它事情:它們支持安全性以及使不同的 Applet 在不同的頁面上運(yùn)行而互不干擾。
Luke Gorrie 編寫的 Echidna 是一個(gè)開放源碼包,它可以使您在單個(gè)虛擬機(jī)上運(yùn)行多個(gè) Java 應(yīng)用程序。(請(qǐng)參閱進(jìn)一步了解和參考資料。)它使用定制的 ClassLoader,通過向每個(gè)應(yīng)用程序提供該類文件的自身副本,以防止應(yīng)用程序互相干擾。
我們的 ClassLoader 示例
了解了 ClassLoader 如何工作以及如何編寫 ClassLoader 之后,我們將創(chuàng)建稱作 CompilingClassLoader (CCL) 的 Classloader。CCL 為我們編譯 Java 代碼,而無需要我們干涉這個(gè)過程。它基本上就類似于直接構(gòu)建到運(yùn)行時(shí)系統(tǒng)中的 "make" 程序。
注:進(jìn)一步了解之前,應(yīng)注意在 JDK 版本 1.2 中已改進(jìn)了 ClassLoader 系統(tǒng)的某些方面(即 Java 2 平臺(tái))。本教程是按 JDK 版本 1.0 和 1.1 寫的,但也可以在以后的版本中運(yùn)行。
Java 2 中 ClassLoader 的變動(dòng)描述了 Java 版本 1.2 中的變動(dòng),并提供了一些詳細(xì)信息,以便修改 ClassLoader 來利用這些變動(dòng)。
------------------------------------------------------------------------------------------------------
第二章.ClassLoader的結(jié)構(gòu)
ClassLoader 的基本目標(biāo)是對(duì)類的請(qǐng)求提供服務(wù)。當(dāng) JVM 需要使用類時(shí),它根據(jù)名稱向 ClassLoader 請(qǐng)求這個(gè)類,然后 ClassLoader 試圖返回一個(gè)表示這個(gè)類的 Class
對(duì)象。
通過覆蓋對(duì)應(yīng)于這個(gè)過程不同階段的方法,可以創(chuàng)建定制的 ClassLoader。
在本章的其余部分,您會(huì)學(xué)習(xí) Java ClassLoader 的關(guān)鍵方法。您將了解每一個(gè)方法的作用以及它是如何適合裝入類文件這個(gè)過程的。您也會(huì)知道,創(chuàng)建自己的 ClassLoader 時(shí),需要編寫什么代碼。
在下一章中,您將會(huì)利用這些知識(shí)來使用我們的 ClassLoader 示例 -- CompilingClassLoader。
方法 loadClass
ClassLoader.loadClass()
是 ClassLoader 的入口點(diǎn)。其特征如下:
Class loadClass( String name, boolean resolve );
name
參數(shù)指定了 JVM 需要的類的名稱,該名稱以包表示法表示,如 Foo
或 java.lang.Object
。
resolve
參數(shù)告訴方法是否需要解析類。在準(zhǔn)備執(zhí)行類之前,應(yīng)考慮類解析。并不總是需要解析。如果 JVM 只需要知道該類是否存在或找出該類的超類,那么就不需要解析。
在 Java 版本 1.1 和以前的版本中,loadClass
方法是創(chuàng)建定制的 ClassLoader 時(shí)唯一需要覆蓋的方法。(Java 2 中 ClassLoader 的變動(dòng)提供了關(guān)于 Java 1.2 中 findClass()
方法的信息。)
方法 defineClass
defineClass
方法是 ClassLoader 的主要訣竅。該方法接受由原始字節(jié)組成的數(shù)組并把它轉(zhuǎn)換成 Class
對(duì)象。原始數(shù)組包含如從文件系統(tǒng)或網(wǎng)絡(luò)裝入的數(shù)據(jù)。
defineClass
管理 JVM 的許多復(fù)雜、神秘和倚賴于實(shí)現(xiàn)的方面 -- 它把字節(jié)碼分析成運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)、校驗(yàn)有效性等等。不必?fù)?dān)心,您無需親自編寫它。事實(shí)上,即使您想要這么做也不能覆蓋它,因?yàn)樗驯粯?biāo)記成最終的。
方法 findSystemClass
findSystemClass
方法從本地文件系統(tǒng)裝入文件。它在本地文件系統(tǒng)中尋找類文件,如果存在,就使用 defineClass
將原始字節(jié)轉(zhuǎn)換成 Class
對(duì)象,以將該文件轉(zhuǎn)換成類。當(dāng)運(yùn)行 Java 應(yīng)用程序時(shí),這是 JVM 正常裝入類的缺省機(jī)制。(Java 2 中 ClassLoader 的變動(dòng)提供了關(guān)于 Java 版本 1.2 這個(gè)過程變動(dòng)的詳細(xì)信息。)
對(duì)于定制的 ClassLoader,只有在嘗試其它方法裝入類之后,再使用 findSystemClass
。原因很簡單:ClassLoader 是負(fù)責(zé)執(zhí)行裝入類的特殊步驟,不是負(fù)責(zé)所有類。例如,即使 ClassLoader 從遠(yuǎn)程的 Web 站點(diǎn)裝入了某些類,仍然需要在本地機(jī)器上裝入大量的基本 Java 庫。而這些類不是我們所關(guān)心的,所以要 JVM 以缺省方式裝入它們:從本地文件系統(tǒng)。這就是 findSystemClass
的用途。
其工作流程如下:
- 請(qǐng)求定制的 ClassLoader 裝入類。
- 檢查遠(yuǎn)程 Web 站點(diǎn),查看是否有所需要的類。
- 如果有,那么好;抓取這個(gè)類,完成任務(wù)。
- 如果沒有,假定這個(gè)類是在基本 Java 庫中,那么調(diào)用
findSystemClass
,使它從文件系統(tǒng)裝入該類。
在大多數(shù)定制 ClassLoaders 中,首先調(diào)用 findSystemClass
以節(jié)省在本地就可以裝入的許多 Java 庫類而要在遠(yuǎn)程 Web 站點(diǎn)上查找所花的時(shí)間。然而,正如,在下一章節(jié)所看到的,直到確信能自動(dòng)編譯我們的應(yīng)用程序代碼時(shí),才讓 JVM 從本地文件系統(tǒng)裝入類。
方法 resolveClass
正如前面所提到的,可以不完全地(不帶解析)裝入類,也可以完全地(帶解析)裝入類。當(dāng)編寫我們自己的 loadClass
時(shí),可以調(diào)用 resolveClass
,這取決于 loadClass
的 resolve
參數(shù)的值。
方法 findLoadedClass
findLoadedClass
充當(dāng)一個(gè)緩存:當(dāng)請(qǐng)求 loadClass
裝入類時(shí),它調(diào)用該方法來查看 ClassLoader 是否已裝入這個(gè)類,這樣可以避免重新裝入已存在類所造成的麻煩。應(yīng)首先調(diào)用該方法。
組裝
讓我們看一下如何組裝所有方法。
我們的 loadClass
實(shí)現(xiàn)示例執(zhí)行以下步驟。(這里,我們沒有指定生成類文件是采用了哪種技術(shù) -- 它可以是從 Net 上裝入、或者從歸檔文件中提取、或者實(shí)時(shí)編譯。無論是哪一種,那是種特殊的神奇方式,使我們獲得了原始類文件字節(jié)。)
- 調(diào)用
findLoadedClass
來查看是否存在已裝入的類。
- 如果沒有,那么采用那種特殊的神奇方式來獲取原始字節(jié)。
- 如果已有原始字節(jié),調(diào)用
defineClass
將它們轉(zhuǎn)換成 Class
對(duì)象。
- 如果沒有原始字節(jié),然后調(diào)用
findSystemClass
查看是否從本地文件系統(tǒng)獲取類。
- 如果
resolve
參數(shù)是 true
,那么調(diào)用 resolveClass
解析 Class
對(duì)象。
- 如果還沒有類,返回
ClassNotFoundException
。
- 否則,將類返回給調(diào)用程序。
推想
現(xiàn)在您已經(jīng)了解了 ClassLoader 的工作原理,現(xiàn)在該構(gòu)建一個(gè)了。在下一章中,我們將討論 CCL。
---------------------------------------------------------------------------------------------
第三章:Compiling ClassLoader
CCL 揭密
我們的 ClassLoader (CCL) 的任務(wù)是確保代碼被編譯和更新。
下面描述了它的工作方式:
- 當(dāng)請(qǐng)求一個(gè)類時(shí),先查看它是否在磁盤的當(dāng)前目錄或相應(yīng)的子目錄。
- 如果該類不存在,但源碼中有,那么調(diào)用 Java 編譯器來生成類文件。
- 如果該類已存在,檢查它是否比源碼舊。如果是,調(diào)用 Java 編譯器來重新生成類文件。
- 如果編譯失敗,或者由于其它原因不能從現(xiàn)有的源碼中生成類文件,返回
ClassNotFoundException
。
- 如果仍然沒有該類,也許它在其它庫中,所以調(diào)用
findSystemClass
來尋找該類。
- 如果還是沒有,則返回
ClassNotFoundException
。
- 否則,返回該類。
Java 編譯的工作方式
在深入討論之前,應(yīng)該先退一步,討論 Java 編譯。通常,Java 編譯器不只是編譯您要求它編譯的類。它還會(huì)編譯其它類,如果這些類是您要求編譯的類所需要的類。
CCL 逐個(gè)編譯應(yīng)用程序中的需要編譯的每一個(gè)類。但一般來說,在編譯器編譯完第一個(gè)類后,CCL 會(huì)查找所有需要編譯的類,然后編譯它。為什么?Java 編譯器類似于我們正在使用的規(guī)則:如果類不存在,或者與它的源碼相比,它比較舊,那么它需要編譯。其實(shí),Java 編譯器在 CCL 之前的一個(gè)步驟,它會(huì)做大部分的工作。
當(dāng) CCL 編譯它們時(shí),會(huì)報(bào)告它正在編譯哪個(gè)應(yīng)用程序上的類。在大多數(shù)的情況下,CCL 會(huì)在程序中的主類上調(diào)用編譯器,它會(huì)做完所有要做的 -- 編譯器的單一調(diào)用已足夠了。
然而,有一種情形,在第一步時(shí)不會(huì)編譯某些類。如果使用 Class.forName
方法,通過名稱來裝入類,Java 編譯器會(huì)不知道這個(gè)類時(shí)所需要的。在這種情況下,您會(huì)看到 CCL 再次運(yùn)行 Java 編譯器來編譯這個(gè)類。在源代碼中演示了這個(gè)過程。
使用 CompilationClassLoader
要使用 CCL,必須以特殊方式調(diào)用程序。不能直接運(yùn)行該程序,如:
% java Foo arg1 arg2
應(yīng)以下列方式運(yùn)行它:
% java CCLRun Foo arg1 arg2
CCLRun 是一個(gè)特殊的存根程序,它創(chuàng)建 CompilingClassLoader 并用它來裝入程序的主類,以確保通過 CompilingClassLoader 來裝入整個(gè)程序。CCLRun 使用 Java Reflection API 來調(diào)用特定類的主方法并把參數(shù)傳遞給它。有關(guān)詳細(xì)信息,請(qǐng)參閱源代碼。
運(yùn)行示例
源碼包括了一組小類,它們演示了工作方式。主程序是 Foo
類,它創(chuàng)建類 Bar
的實(shí)例。類 Bar
創(chuàng)建另一個(gè)類 Baz
的實(shí)例,它在 baz
包內(nèi),這是為了展示 CCL 是如何處理子包里的代碼。Bar
也是通過名稱裝入的,其名稱為 Boo
,這用來展示它也能與 CCL 工作。
每個(gè)類都聲明已被裝入并運(yùn)行。現(xiàn)在用源代碼來試一下。編譯 CCLRun 和 CompilingClassLoader。確保不要編譯其它類(Foo
、Bar
、Baz
和 Boo
),否則將不會(huì)使用 CCL,因?yàn)檫@些類已經(jīng)編譯過了。
% java CCLRun Foo arg1 arg2
CCL: Compiling Foo.java...
foo! arg1 arg2
bar! arg1 arg2
baz! arg1 arg2
CCL: Compiling Boo.java...
Boo!
請(qǐng)注意,首先調(diào)用編譯器,Foo.java
管理 Bar
和 baz.Baz
。直到 Bar
通過名稱來裝入 Boo
時(shí),被調(diào)用它,這時(shí) CCL 會(huì)再次調(diào)用編譯器來編譯它。
--------------------------------------------------------------------------------------
第四章:java2 中ClassLoader的變動(dòng)
概述
在 Java 版本 1.2 和以后的版本中,對(duì) ClassLoader 做了一些改進(jìn)。任何為老系統(tǒng)編寫的代碼可以在新版本中運(yùn)行,但新系統(tǒng)為您提供了一些便利。
新模型是委托模型,這意味著如果 ClassLoader 不能找到類,它會(huì)請(qǐng)求父代 ClassLoader 來執(zhí)行此項(xiàng)任務(wù)。所有 ClassLoaders 的根是系統(tǒng) ClassLoader,它會(huì)以缺省方式裝入類 -- 即,從本地文件系統(tǒng)。
loadClass
的缺省實(shí)現(xiàn)
定制編寫的 loadClass
方法一般嘗試幾種方式來裝入所請(qǐng)求的類,如果您編寫許多類,會(huì)發(fā)現(xiàn)一次次地在相同的、很復(fù)雜的方法上編寫變量。
在 Java 1.2 中 loadClass
的實(shí)現(xiàn)嵌入了大多數(shù)查找類的一般方法,并使您通過覆蓋 findClass
方法來定制它,在適當(dāng)?shù)臅r(shí)候 findClass
會(huì)調(diào)用 loadClass
。
這種方式的好處是您可能不一定要覆蓋 loadClass
;只要覆蓋 findClass
就行了,這減少了工作量。
新方法:findClass
loadClass
的缺省實(shí)現(xiàn)調(diào)用這個(gè)新方法。findClass
的用途包含您的 ClassLoader 的所有特殊代碼,而無需要復(fù)制其它代碼(例如,當(dāng)專門的方法失敗時(shí),調(diào)用系統(tǒng) ClassLoader)。
新方法:getSystemClassLoader
如果覆蓋 findClass
或 loadClass
,getSystemClassLoader
使您能以實(shí)際 ClassLoader
對(duì)象來訪問系統(tǒng) ClassLoader(而不是固定的從 findSystemClass
調(diào)用它)。
新方法:getParent
為了將類請(qǐng)求委托給父代 ClassLoader,這個(gè)新方法允許 ClassLoader 獲取它的父代 ClassLoader。當(dāng)使用特殊方法,定制的 ClassLoader 不能找到類時(shí),可以使用這種方法。
父代 ClassLoader 被定義成創(chuàng)建該 ClassLoader 所包含代碼的對(duì)象的 ClassLoader。
----------------------------------------------------------------------------------
第五章.源代碼
CompilingClassLoader.java
以下是 CompilingClassLoader.java 的源代碼
// $Id$
import java.io.*;
/*
A CompilingClassLoader compiles your Java source on-the-fly. It checks
for nonexistent .class files, or .class files that are older than their
corresponding source code.*/
public class CompilingClassLoader extends ClassLoader
{
// Given a filename, read the entirety of that file from disk
// and return it as a byte array.
private byte[] getBytes( String filename ) throws IOException {
// Find out the length of the file
File file = new File( filename );
long len = file.length();
// Create an array that's just the right size for the file's
// contents
byte raw[] = new byte[(int)len];
// Open the file
FileInputStream fin = new FileInputStream( file );
// Read all of it into the array; if we don't get all,
// then it's an error.
int r = fin.read( raw );
if (r != len)
throw new IOException( "Can't read all, "+r+" != "+len );
// Don't forget to close the file!
fin.close();
// And finally return the file contents as an array
return raw;
}
// Spawn a process to compile the java source code file
// specified in the 'javaFile' parameter. Return a true if
// the compilation worked, false otherwise.
private boolean compile( String javaFile ) throws IOException {
// Let the user know what's going on
System.out.println( "CCL: Compiling "+javaFile+"..." );
// Start up the compiler
Process p = Runtime.getRuntime().exec( "javac "+javaFile );
// Wait for it to finish running
try {
p.waitFor();
} catch( InterruptedException ie ) { System.out.println( ie ); }
// Check the return code, in case of a compilation error
int ret = p.exitValue();
// Tell whether the compilation worked
return ret==0;
}
// The heart of the ClassLoader -- automatically compile
// source as necessary when looking for class files
public Class loadClass( String name, boolean resolve )
throws ClassNotFoundException {
// Our goal is to get a Class object
Class clas = null;
// First, see if we've already dealt with this one
clas = findLoadedClass( name );
//System.out.println( "findLoadedClass: "+clas );
// Create a pathname from the class name
// E.g. java.lang.Object => java/lang/Object
String fileStub = name.replace( '.', '/' );
// Build objects pointing to the source code (.java) and object
// code (.class)
String javaFilename = fileStub+".java";
String classFilename = fileStub+".class";
File javaFile = new File( javaFilename );
File classFile = new File( classFilename );
//System.out.println( "j "+javaFile.lastModified()+" c "+
// classFile.lastModified() );
// First, see if we want to try compiling. We do if (a) there
// is source code, and either (b0) there is no object code,
// or (b1) there is object code, but it's older than the source
if (javaFile.exists() &&
(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
try {
// Try to compile it. If this doesn't work, then
// we must declare failure. (It's not good enough to use
// and already-existing, but out-of-date, classfile)
if (!compile( javaFilename ) || !classFile.exists()) {
throw new ClassNotFoundException( "Compile failed: "+javaFilename );
}
} catch( IOException ie ) {
// Another place where we might come to if we fail
// to compile
throw new ClassNotFoundException( ie.toString() );
}
}
// Let's try to load up the raw bytes, assuming they were
// properly compiled, or didn't need to be compiled
try {
// read the bytes
byte raw[] = getBytes( classFilename );
// try to turn them into a class
clas = defineClass( name, raw, 0, raw.length );
} catch( IOException ie ) {
// This is not a failure! If we reach here, it might
// mean that we are dealing with a class in a library,
// such as java.lang.Object
}
//System.out.println( "defineClass: "+clas );
// Maybe the class is in a library -- try loading
// the normal way
if (clas==null) {
clas = findSystemClass( name );
}
//System.out.println( "findSystemClass: "+clas );
// Resolve the class, if any, but only if the "resolve"
// flag is set to true
if (resolve && clas != null)
resolveClass( clas );
// If we still don't have a class, it's an error
if (clas == null)
throw new ClassNotFoundException( name );
// Otherwise, return the class
return clas;
}
}
CCRun.java
以下是 CCRun.java 的源代碼
// $Id$
import java.lang.reflect.*;
/*
CCLRun executes a Java program by loading it through a
CompilingClassLoader.
*/
public class CCLRun
{
static public void main( String args[] ) throws Exception {
// The first argument is the Java program (class) the user
// wants to run
String progClass = args[0];
// And the arguments to that program are just
// arguments 1..n, so separate those out into
// their own array
String progArgs[] = new String[args.length-1];
System.arraycopy( args, 1, progArgs, 0, progArgs.length );
// Create a CompilingClassLoader
CompilingClassLoader ccl = new CompilingClassLoader();
// Load the main class through our CCL
Class clas = ccl.loadClass( progClass );
// Use reflection to call its main() method, and to
// pass the arguments in.
// Get a class representing the type of the main method's argument
Class mainArgType[] = { (new String[0]).getClass() };
// Find the standard main method in the class
Method main = clas.getMethod( "main", mainArgType );
// Create a list containing the arguments -- in this case,
// an array of strings
Object argsArray[] = { progArgs };
// Call the method
main.invoke( null, argsArray );
}
}
Foo.java
以下是 Foo.java 的源代碼
// $Id$
public class Foo
{
static public void main( String args[] ) throws Exception {
System.out.println( "foo! "+args[0]+" "+args[1] );
new Bar( args[0], args[1] );
}
}
Bar.java
以下是 Bar.java 的源代碼
// $Id$
import baz.*;
public class Bar
{
public Bar( String a, String b ) {
System.out.println( "bar! "+a+" "+b );
new Baz( a, b );
try {
Class booClass = Class.forName( "Boo" );
Object boo = booClass.newInstance();
} catch( Exception e ) {
e.printStackTrace();
}
}
}
baz/Baz.java
以下是 baz/Baz.java 的源代碼
// $Id$
package baz;
public class Baz
{
public Baz( String a, String b ) {
System.out.println( "baz! "+a+" "+b );
}
}
Boo.java
以下是 Boo.java 的源代碼
// $Id$
public class Boo
{
public Boo() {
System.out.println( "Boo!" );
}
}