瞭解 JAVA classloader

333111444發表於2009-02-17

什麼是 ClassLoader?


在流行的商業化程式語言中,Java 語言由於在 Java 虛擬機器 (JVM) 上執行而顯得與眾不同。這意味著已編譯的程式是一種非凡的、獨立於平臺的格式,並非依靠於它們所執行的機器。在很大程度上,這種格式不同於傳統的可執行程式格式。

與 C 或 C++ 編寫的程式不同,Java 程式並不是一個可執行檔案,而是由許多獨立的類檔案組成,每一個檔案對應於一個 Java 類。
此外,這些類檔案並非立即全部都裝入記憶體,而是根據程式需要裝入記憶體。ClassLoader 是 JVM 中將類裝入記憶體的那部分。
而且,Java ClassLoader 就是用 Java 語言編寫的。這意味著建立您自己的 ClassLoader 非常輕易,不必瞭解 JVM 的微小細節。


為什麼編寫 ClassLoader?


假如 JVM 已經有一個 ClassLoader,那麼為什麼還要編寫另一個呢?問得好。預設的 ClassLoader 只知道如何從本地檔案系統裝入類檔案。不過這隻適合於常規情況,即已全部編譯完 Java 程式,並且計算機處於等待狀態。

但 Java 語言最具新意的事就是 JVM 可以非常輕易地從那些非本地硬碟或從網路上獲取類。例如,瀏覽者可以使用定製的 ClassLoader 從 Web 站點裝入可執行內容。

有許多其它方式可以獲取類檔案。除了簡單地從本地或網路裝入檔案以外,可以使用定製的 ClassLoader 完成以下任務:
在執行非置信程式碼之前,自動驗證數字簽名
使用使用者提供的密碼透明地解密程式碼
動態地建立符合使用者特定需要的定製化構建類
任何您認為可以生成 Java 位元組碼的內容都可以整合到應用程式中。

[@more@]

定製 ClassLoader 示例


假如使用過 JDK 或任何基於 Java 瀏覽器中的 Applet 檢視器,那麼您差不多肯定使用過定製的 ClassLoader。

Sun 最初發布 Java 語言時,其中最令人興奮的一件事是觀看這項新技術是如何執行在執行時從遠端的 Web 伺服器裝入的程式碼。(此外,還有更令人興奮的事 -- Java 技術提供了一種便於編寫程式碼的強大語言。)更一些令人激動的是它可以執行從遠端 Web 伺服器透過 HTTP 連線傳送過來的位元組碼。

此項功能歸功於 Java 語言可以安裝定製 ClassLoader。Applet 檢視器包含一個 ClassLoader,它不在本地檔案系統中尋找類,而是訪問遠端伺服器上的 Web 站點,經過 HTTP 裝入原始的位元組碼檔案,並把它們轉換成 JVM 內的類。

瀏覽器和 Applet 檢視器中的 ClassLoaders 還可以做其它事情:它們支援安全性以及使不同的 Applet 在不同的頁面上執行而互不干擾。

Luke Gorrie 編寫的 Echidna 是一個開放原始碼包,它可以使您在單個虛擬機器上執行多個 Java 應用程式。它使用定製的 ClassLoader,透過向每個應用程式提供該類檔案的自身副本,以防止應用程式互相干擾。


我們的 ClassLoader 示例


瞭解了 ClassLoader 如何工作以及如何編寫 ClassLoader 之後,我們將建立稱作 CompilingClassLoader (CCL) 的 Classloader。CCL 為我們編譯 Java 程式碼,而無需要我們干涉這個過程。它基本上就類似於直接構建到執行時系統中的 "make" 程式。
注:進一步瞭解之前,應注重在 JDK 版本 1.2 中已改進了 ClassLoader 系統的某些方面(即 Java 2 平臺)。本教程是按 JDK 版本 1.0 和 1.1 寫的,但也可以在以後的版本中執行。

Java 2 中 ClassLoader 的變動描述了 Java 版本 1.2 中的變動,並提供了一些具體資訊,以便修改 ClassLoader 來利用這些變動。

ClassLoader 的基本目標是對類的請求提供服務。當 JVM 需要使用類時,它根據名稱向 ClassLoader 請求這個類,然後 ClassLoader 試圖返回一個表示這個類的 Class 物件。 透過覆蓋對應於這個過程不同階段的方法,可以建立定製的 ClassLoader。

在本文的其餘部分,您會學習 Java ClassLoader 的要害方法。您將瞭解每一個方法的作用以及它是如何適合裝入類檔案這個過程的。您也會知道,建立自己的 ClassLoader 時,需要編寫什麼程式碼。

在下文中,您將會利用這些知識來使用我們的 ClassLoader 示例 -- CompilingClassLoader。


方法 loadClass

ClassLoader.loadClass() 是 ClassLoader 的入口點。其特徵如下:
Class loadClass( String name, boolean resolve );
name 引數指定了 JVM 需要的類的名稱,該名稱以包表示法表示,如 Foo 或 java.lang.Object。 resolve 引數告訴方法是否需要解析類。在預備執行類之前,應考慮類解析。並不總是需要解析。假如 JVM 只需要知道該類是否存在或找出該類的超類,那麼就不需要解析。
在 Java 版本 1.1 和以前的版本中,loadClass 方法是建立定製的 ClassLoader 時唯一需要覆蓋的方法。(Java 2 中 ClassLoader 的變動提供了關於 Java 1.2 中 findClass() 方法的資訊。)


方法 defineClass


defineClass 方法是 ClassLoader 的主要訣竅。該方法接受由原始位元組組成的陣列並把它轉換成 Class 物件。原始陣列包含如從檔案系統或網路裝入的資料。

defineClass 治理 JVM 的許多複雜、神秘和倚賴於實現的方面 -- 它把位元組碼分析成執行時資料結構、校驗有效性等等。不必擔心,您無需親自編寫它。事實上,即使您想要這麼做也不能覆蓋它,因為它已被標記成最終的。


方法 findSystemClass


findSystemClass 方法從本地檔案系統裝入檔案。它在本地檔案系統中尋找類檔案,假如存在,就使用 defineClass 將原始位元組轉換成 Class 物件,以將該檔案轉換成類。當執行 Java 應用程式時,這是 JVM 正常裝入類的預設機制。(Java 2 中 ClassLoader 的變動提供了關於 Java 版本 1.2 這個過程變動的具體資訊。)

對於定製的 ClassLoader,只有在嘗試其它方法裝入類之後,再使用 findSystemClass。原因很簡單:ClassLoader 是負責執行裝入類的非凡步驟,不是負責所有類。例如,即使 ClassLoader 從遠端的 Web 站點裝入了某些類,仍然需要在本地機器上裝入大量的基本 Java 庫。而這些類不是我們所關心的,所以要 JVM 以預設方式裝入它們:從本地檔案系統。這就是 findSystemClass 的用途。

其工作流程如下:
請求定製的 ClassLoader 裝入類。
檢查遠端 Web 站點,檢視是否有所需要的類。
假如有,那麼好;抓取這個類,完成任務。
假如沒有,假定這個類是在基本 Java 庫中,那麼呼叫 findSystemClass,使它從檔案系統裝入該類。


在大多數定製 ClassLoaders 中,首先呼叫 findSystemClass 以節省在本地就可以裝入的許多 Java 庫類而要在遠端 Web 站點上查詢所花的時間。然而,正如,在下一章節所看到的,直到確信能自動編譯我們的應用程式程式碼時,才讓 JVM 從本地檔案系統裝入類。


方法 resolveClass
正如前面所提到的,可以不完全地(不帶解析)裝入類,也可以完全地(帶解析)裝入類。當編寫我們自己的 loadClass 時,可以呼叫 resolveClass,這取決於 loadClass 的 resolve 引數的值。


方法 findLoadedClass
findLoadedClass 充當一個快取:當請求 loadClass 裝入類時,它呼叫該方法來檢視 ClassLoader 是否已裝入這個類,這樣可以避免重新裝入已存在類所造成的麻煩。應首先呼叫該方法。


組裝
讓我們看一下如何組裝所有方法。
我們的 loadClass 實現示例執行以下步驟。(這裡,我們沒有指定生成類檔案是採用了哪種技術 -- 它可以是從 Net 上裝入、或者從歸檔檔案中提取、或者實時編譯。無論是哪一種,那是種非凡的神奇方式,使我們獲得了原始類檔案位元組。)


CCL 揭密
我們的 ClassLoader (CCL) 的任務是確保程式碼被編譯和更新。
下面描述了它的工作方式:
當請求一個類時,先檢視它是否在磁碟的當前目錄或相應的子目錄。
假如該類不存在,但原始碼中有,那麼呼叫 Java 編譯器來生成類檔案。
假如該類已存在,檢查它是否比原始碼舊。假如是,呼叫 Java 編譯器來重新生成類檔案。
假如編譯失敗,或者由於其它原因不能從現有的原始碼中生成類檔案,返回 ClassNotFoundException。
假如仍然沒有該類,也許它在其它庫中,所以呼叫 findSystemClass 來尋找該類。
假如還是沒有,則返回 ClassNotFoundException。
否則,返回該類。
呼叫 findLoadedClass 來檢視是否存在已裝入的類。
假如沒有,那麼採用那種非凡的神奇方式來獲取原始位元組。
假如已有原始位元組,呼叫 defineClass 將它們轉換成 Class 物件。
假如沒有原始位元組,然後呼叫 findSystemClass 檢視是否從本地檔案系統獲取類。
假如 resolve 引數是 true,那麼呼叫 resolveClass 解析 Class 物件。
假如還沒有類,返回 ClassNotFoundException。
否則,將類返回給呼叫程式。


Java 編譯的工作方式
在深入討論之前,應該先退一步,討論 Java 編譯。通常,Java 編譯器不只是編譯您要求它編譯的類。它還會編譯其它類,假如這些類是您要求編譯的類所需要的類。
CCL 逐個編譯應用程式中的需要編譯的每一個類。但一般來說,在編譯器編譯完第一個類後,CCL 會查詢所有需要編譯的類,然後編譯它。為什麼?Java 編譯器類似於我們正在使用的規則:假如類不存在,或者與它的原始碼相比,它比較舊,那麼它需要編譯。其實,Java 編譯器在 CCL 之前的一個步驟,它會做大部分的工作。

當 CCL 編譯它們時,會報告它正在編譯哪個應用程式上的類。在大多數的情況下,CCL 會在程式中的主類上呼叫編譯器,它會做完所有要做的 -- 編譯器的單一呼叫已足夠了。

然而,有一種情形,在第一步時不會編譯某些類。假如使用 Class.forName 方法,透過名稱來裝入類,Java 編譯器會不知道這個類時所需要的。在這種情況下,您會看到 CCL 再次執行 Java 編譯器來編譯這個類。在原始碼中演示了這個過程。


使用 CompilationClassLoader
要使用 CCL,必須以非凡方式呼叫程式。不能直接執行該程式,如: % java Foo arg1 arg2
應以下列方式執行它:
% java CCLRun Foo arg1 arg2


CCLRun 是一個非凡的存根程式,它建立 CompilingClassLoader 並用它來裝入程式的主類,以確保透過 CompilingClassLoader 來裝入整個程式。CCLRun 使用 Java Reflection API 來呼叫特定類的主方法並把引數傳遞給它。有關具體資訊,請參閱原始碼。


執行示例
原始碼包括了一組小類,它們演示了工作方式。主程式是 Foo 類,它建立類 Bar 的例項。類 Bar 建立另一個類 Baz 的例項,它在 baz 包內,這是為了展示 CCL 是如何處理子包裡的程式碼。Bar 也是透過名稱裝入的,其名稱為 Boo,這用來展示它也能與 CCL 工作。

每個類都宣告已被裝入並執行。現在用原始碼來試一下。編譯 CCLRun 和 CompilingClassLoader。確保不要編譯其它類(Foo、Bar、Baz 和 Boo),否則將不會使用 CCL,因為這些類已經編譯過了。

% java CCLRun Foo arg1 arg2
CCL: Compiling Foo.java...
foo! arg1 arg2
bar! arg1 arg2
baz! arg1 arg2
CCL: Compiling Boo.java...
Boo!


請注重,首先呼叫編譯器,Foo.java 治理 Bar 和 baz.Baz。直到 Bar 透過名稱來裝入 Boo 時,被呼叫它,這時 CCL 會再次呼叫編譯器來編譯它。
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!" );
}
}

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12058779/viewspace-1017483/,如需轉載,請註明出處,否則將追究法律責任。

相關文章