第一部分. 提示我需要讀這篇文章嗎?
Java類別載入器對Java系統的運作是至關重要的,但是卻常常被我們忽略。 Java類別載入器負載在執行時間尋找和載入類別。自訂類別載入器可以完全改變類別的載入方式,以自己喜歡的方式來個性化你的Java虛擬機器。本文簡要的介紹Java類別載入器,然後透過一個建構自訂類別載入器的範例來說明,這個類別載入器在載入類別前會自動編譯程式碼。你將學到類別載入器到底是做什麼的,如何建立你自己的類別載入器。只要你有一些基本的Java知識,知道如何建立、編譯、執行一個命令列Java程式以及一些Java類別檔案的基本概念,你就可以理解本文的內容了。讀完本文,你應該能夠:
* 擴張Java虛擬機器的功能
* 建立一個自訂的類別載入器
* 如何把自訂的類別載入器整合到你的應用程式中
* 修改你的類別載入器以相容Java2
第二部分. 簡介類別載入器是什麼?
Java和其他語言不同的是,Java是運行於Java虛擬機器(JVM)。這意味著編譯後的程式碼是以一種和平台無關的格式保存的,而不是某種特定的機器上運行的格式。這種格式和傳統的可執行程式碼格式有許多重要的區別。具體來說,不同於C或C++程序,Java程式不是一個獨立的可執行文件,而是由很多分開的類別文件組成,每個類別文件對應一個Java類別。 另外,這些類別文件並不是馬上載入到內存,而是當程式需要的時候才會載入。 類別載入器就是Java虛擬機器中用來把類別載入到記憶體的工具。而且,Java類別載入器也是用Java實現的。這樣你就不需要對Java虛擬機有深入的理解就可以輕鬆創建自己的類別載入器了。
為什麼要創建類別載入器?
既然Java虛擬金已經有了類別載入器,我們還要自己創建其他的呢?問得好。預設的類別載入器只知道如何從本機系統載入類別。當你的程式完全在本機編譯的話,預設的類別載入器一般都運作的很好。但是Java中最令人興奮的地方之一就是很容易的從網路上而不只是本地加載類別。
舉個例子,瀏覽器可以透過自訂的類別載入器載入類別。 還有很多載入類別的方式。除了簡單的從本地或網路外,你還可以透過自訂Java中最令人興奮的地方之一:
* 執行非信任代碼前自動驗證數位簽名
* 根據使用者提供的密碼解密代碼
* 根據用戶的需要動態的創建類你關心的任何東西都能方便的以字節碼的形式集成到你的應用中自定義類加載器的例子如果你已經使用過JDK(Java軟體開發包)中的appletviewer(小應用程式瀏覽器)或其他
Java嵌入式瀏覽器,你就已經使用了自訂類別載入器了。 Sun剛發布Java語言的時候,最令人興奮的一件事就是觀看Java如何執行從遠端網站下載的程式碼。執行從遠端站點透過HTT
P連結傳送來的字節碼看起來有點不可思議。之所以能夠運作,因為Java有安裝自訂類別載入器的能力。小型應用程式瀏覽器包含了一個類別載入器,這個類別載入器不從本地找Java類,而是存取遠端伺服器,透過HTTP載入原始字節碼文件,然後在Java虛擬機器中轉化為Java類別。當然類別載入器也做了其他的很多事情:他們阻止不安全的Java類,而且保持不同頁面上的不同小程式不會互相干擾。 Luke Gorrie寫的一個包Echidna是一個開放的Java軟體包,他允許在一個Java虛擬機中安全的運行多個Java應用程式。它透過使用自訂類別載入器給每個應用程式一份類別檔案的拷貝來阻止應用程式之間的干擾。
我們的類別載入器範例我們知道了類別載入器是如何運作的,也知道如何定義自己的類別載入器了,接下來我們建立一個名字為CompilingClassLoader (CCL)的自訂類別載入器。 CCL為我們做編譯工作,就不用自己手動編譯了。 這基本上相當於有一個"make"程式建置到我們的運行環境。
注意:在我們進行下一步之前,有必要先搞清楚一些相關的概念。
系統在JDK版本1.2(也就是我們所說的Java 2平台)都得到改進。本文是在JDK1.0和1.1的版本下寫的,但是所有的東西都能在後來的版本工作。 ClassLoader也在Java2種有所改進,
第五部分有詳細介紹。
第三部分.ClassLoader的結構總攬類別載入器的基本目的是服務對Java類別的請求。 Java虛擬機器需要一個類別的時候,就把一個類別名稱給類別載入器,然後類別載入器試著傳回一個對應的類別實例。可以透過在不同的階段覆寫相應的方法來建立自訂的類別載入器。接下來我們將了解類別載入器的一些主要方法。你會明白這些方法是做什麼的,他們在載入類別文件的時候是如何運作的。你還將知道創建自訂類別載入器的時候需要寫哪些程式碼。在下一部分,你將利用這些知識和我們自訂的CompilingCl
assLoader一起工作。
方法loadClass
ClassLoader.loadClass() 是ClassLoader的入口點。方法簽名如下:
Class loadClass( String name, boolean resolve);
參數name指定Java虛擬機器所需的類別的全名(含包名),例如Foo或java.lang.Object。
參數resolve指定該類別是否需要解析你可以把類別的解析理解為完全為運行做好準備。解析一般都不需要。如果Java虛擬機器只想知道這個類別是否存在或想知道它的父類別的話,解析就完全沒有必要了。 在Java1.1和它以前的版本,如果要自訂類別載入器,loadClass方法是唯一需要在子類別中覆寫的方法.
(ClassLoader在Java1.2中有所改變,提供了方法findClass())。
方法defineClass
defineClass 是ClassLoader中一個很神祕的方法。這個方法透過一個位元組數組來建構類別實例。這個包含資料的原始位元組數組可能來自檔案系統,也可能是來自網路。 defineClass 顯示了Java虛擬機的複雜性,神秘性和平台依賴性-它透過解釋字節碼把它轉化為運行時資料結構,檢查有效性等等。但不用擔心,這些都不用你去實現。其實,你根本無法覆蓋它,
因為該方法被關鍵字final修飾。
方法findSystemClass
findSystemClass方法從本機系統載入檔案。它在本地系統尋找類別文件,如果找到了,調用
defineClass把原始位元組數組轉換成類別物件。這是執行Java應用時Java虛擬機器載入類別的預設機制。對於自訂類別載入器,只有在我們無法載入之後才需要用findSystemClass。 原因很簡單: 我們的類別載入器負責執行類別載入中的某些特定的步驟,但並不是對所有的類別。比如,
即使我們的類別載入器從遠端網站載入了某些類,仍然有許多基本的類別要從本機系統載入。
這些類別不是我們關心的,所以我們讓Java虛擬機器以預設的方式載入他們:從本機系統。這就是findSystemClass所做的事情。整個過程大致如下:
* Java虛擬機器請求我們自訂的類別載入器載入類別。
* 我們檢查遠端站點是否有這個需要載入的類別。
* 如果有,我們取得這個類別。
* 如果沒有,我們認為這個是類別在基本類別庫中,呼叫findSystemClass從檔案系統載入。
在大多數自訂類別載入器中,你應該先呼叫findSystemClass來節省從遠端尋找的時間。
實際上,正如我們將在下一部分看到的,只有當我們確定我們已經自動編譯完我們的程式碼後才允許Java虛擬機器從本機檔案系統載入類別。
方法resolveClass
如同上面所說的,類別記載可以分為部分載入(不解析)和完全載入(包括解析)。我們建立自訂類別載入器的時候,可能要呼叫resolveClass。
方法findLoadedClass
findLoadedClass實作一個快取:當要求loadClass來載入一個類別的時候,可以先呼叫這個方法看看這個類別是否已經被載入,防止重新載入一個已經載入的類別。這個方法必須先被調用,我們來看看這些方法是如何組織在一起的。
我們的範例實作loadClass執行以下的步驟。 (我們不指定通過某種具體的技術獲得類文件,-它可能從網絡,從壓縮包或動態編譯的。無論如何,我們獲得的是原始字節碼文件)
* 呼叫findLoadedClass檢查這個類別是否已經載入。
* 如果沒有加載,我們透過某種方式獲得原始位元組數組。
* 假如已經取得該數組,呼叫defineClass把它轉換成類別物件。
* 如果無法取得該原始位元組數組,請呼叫findSystemClass 檢查是否可以從本機檔案系統中記載。
* 如果參數resolve為true,呼叫resolveClass來解析類別物件。
* 如果還沒找到類,拋出一個ClassNotFoundException異常。
* 否則,回傳這個類別。
現在我們對類別載入器的應用知識有個較全面的了解,可以建立自訂類別載入器了。在下一部分,我們將討論CCL。
第四部分. CompilingClassLoader
CCL給我們展示了類別載入器的功能, CCL的目的是讓我們的程式碼能夠自動編譯和更新。下面描述它是怎麼運作的:
* 當有一個類別的請求時,先檢查磁碟的目前目錄和子目錄上是否存在這個類別檔案。
* 如果沒有類別文件,但是卻有原始碼文件,呼叫Java編譯器編譯生成類別文件。
* 如果類別檔案已經存在,檢查該類別檔案是否比原始程式碼檔案陳舊。如果類別檔案比原始程式碼檔案陳舊,呼叫Java編譯器重新產生類別檔案。
* 如果編譯失敗,或因其他原因而無法從原始檔產生類別文件,拋出異常ClassNotFou
ndException。
* 如果還沒取得這個類,可能存在其他的類別庫裡,呼叫findSystemClass看是否能找到。
* 如果沒有找到,拋出異常ClassNotFoundException。
* 否則,返回該類別。
Java編譯是如何實現的?
在我們進一步討論之前,我們需要先弄清楚Java的編譯過程。通常,Java編譯器會不只編譯指定的那些類別。如果指定的那些類別需要的話,它也會編譯其它的一些相關類別。 CCL會一個一個的編譯我們在應用程式中需要編譯的那些類別。不過,一般來說,編譯器編譯完第一個類別後,
CCL將會發現其實其他需要的相關類別已經被編譯了。為什麼呢? Java編譯器使用我們差不多的規則:如果一個了類別不存在或是原始檔已經被更新,就會編譯這個類別。 Java編譯器基本上比CCL早了一步,大部分工作都被Java編譯器完成了。我們看起來就像是CCL在編譯這些類別。
在大多數情況下,你會發現它是在主函數類別中呼叫編譯器,就僅僅這些而已--簡單的一個呼叫就夠了。 不過有一種特殊情況,這些類別在第一次出現的時候不會編譯。如果你根據類別名稱載入一個類,使用方法Class.forName,Java編譯器並不知道是否需要這個類別。在這種情況下,
你發現CCL再次呼叫編譯器來編譯該類別。第六部分的程式碼說明了這個過程。
使用CompilationClassLoader
為了使用CCL,我們不能直接運行我們的程序,必須以一種特殊的方式運行,就像這樣:
% java Foo arg1 arg2
我們這樣運行它:
% java CCLRun Foo arg1 arg2
CCLRun是一個特殊的存根程序,它來創建CompilingClassLoader 並且用它來加載我們的主函數類,這樣可以確保所有的整個程序都是由CompilingClassLoader加載的。 CCLRun利用Ja
va反射API來呼叫主函數類別的主函數並且給這個函數傳遞參數。想了解更多,參考第六部分的原始碼。
運行範例我們示範一下整個過程式怎麼運作的。
主程式是一個叫做Foo的類,它建立一個類別Bar的實例。這個Bar實例又建立一個類別Baz的實例,類別Baz存在於套件baz中,這是為了示範CCL如何從子套件載入類別。 Bar也根據類別名稱載入類別Boo
,這個也是CCL完成的。所有的類別都載入了就可以運行了。利用第六章的原始碼來執行這個程式。編譯CCLRun和CompilingClassLoader。確保你沒有編譯其它的類別(Foo, Bar, Baz, a
nd 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一起編譯了俄。而類別Boo
直道需要載入的時候,CCL才再次呼叫編譯器來編譯它。
第五部分.Java2中對類別載入器的改進概覽在Java1.2和以後的版本中, 類別載入器有了很大的改進。以前的程式碼仍然可以工作, 但是新的系統讓我們的實作更容易。這種新模型就是代理委託模型,就是說如果這個類別載入器找不到某個類,它會讓他的父類載入器來找。系統類別載入器是所有類別載入器的祖先, 系統類別載入器透過預設的方式載入類別--也就是從本地檔案系統載入。覆蓋loadClass方法一般都嘗試幾種方式來加載類,如果你寫了很多類加載器,你會發現你只是一次又一次在這個複雜的方法中作一些修改而已。 Java1.2種loadClass的預設實作包含了尋找類別的最普通的途徑,允許你覆寫findClass方法,loadClass在適當的是否呼叫findClass方法。這樣做的好處是你不需要涵蓋loadClass,你只需要涵蓋findClass,這樣可以減少工作量。
新增方法: findClass
這個方法被loadClass的預設實作呼叫。 findClass的目標是包含所有類別載入器特定的程式碼,
而不需要重複這些程式碼(例如在指定的方法失敗的時候呼叫系統類別載入器)。
新增方法: getSystemClassLoader
無論你是否覆寫方法findClass和loadClass, 方法getSystemClassLoader都可以直接存取系統類別載入器(而不是透過findSystemClass間接的存取)。
新增方法: getParent
為了把請求委託給父類別載入器,透過這個方法可以獲得這個類別載入器的父類別載入器。當自訂類別載入器中的特定方法無法找到類別的時候你可能會把請求委託給父類別載入器。類別載入器的父類別載入器包含建立這個類別載入器的程式碼。
第六部分. 原始碼
CompilingClassLoader.java
以下是檔案CompilingClassLoader.java內容
import java.io.*;
/*
CompilingClassLoader動態的編譯Java原始檔。它檢查.class檔案是否存在,.class檔案是否比原始檔案陳舊。
*/
public class CompilingClassLoader extends ClassLoader
{
// 指定一個檔案名,從磁碟讀取整個檔案內容,傳回位元組數組。
private byte[] getBytes( String filename ) throws IOException {
// 取得檔案大小。
File file = new File( filename );
長 len = file.length();
//建立一個陣列剛好可以存放檔案的內容。
byte raw[] = new byte[(int)len];
// 開啟檔案
FileInputStream fin = new FileInputStream( file );
// 讀取所有內容,如果沒辦法讀取,表示發生了一個錯誤。
int r = fin.read( raw );
if (r != len)
throw new IOException( "Can''''t read all, "+r+" != "+len );
// 別忘了關閉檔案。
fin.close();
// 傳回這個陣列。
return raw;
}
// 產生一個行程來編譯指定的Java原始文件,制定文件參數.如果編譯成功回傳true,否者,
// 回傳false。
private boolean compile( String javaFile ) throws IOException {
// 顯示當前進度
System.out.println( "CCL: Compiling "+javaFile+"..." );
// 啟動編譯器
Process p = Runtime.getRuntime().exec( "javac "+javaFile );
// 等待編譯結束
try {
p.waitFor();
} catch( InterruptedException ie ) { System.out.println( ie ); }
// 檢查回傳碼,看編譯是否出錯。
int ret = p.exitValue();
// 回傳編譯是否成功。
return ret==0;
}
// 類別載入器的核心程式碼-載入類別在需要的時候自動編譯原始檔。
public Class loadClass( String name, boolean resolve )
throws ClassNotFoundException {
//我們的目的是要取得一個類別物件。
Class clas = null;
// 首先,檢查是否已經出理過這個類別。
clas = findLoadedClass( name );
//System.out.println( "findLoadedClass: "+clas );
// 透過類別名稱取得路徑名稱例如:java.lang.Object => java/lang/Object
String fileStub = name.replace( ''''.'''', ''''/'''' );
// 建構指向原始檔案和類別檔案的物件。
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() );
// 首先,判斷是否需要編譯。如果來源文件存在而類別文件不存在,或者都存在,但是來源文件
// 較新,說明需要編譯。
if (javaFile.exists() &&(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
try {
// 編譯,如果編譯失敗,我們必須宣告失敗原因(僅僅使用陳舊的類別是不夠的)。
if (!compile( javaFilename ) || !classFile.exists()) {
throw new ClassNotFoundException( "Compile failed: "+javaFilename );
}
} catch( IOException ie ) {
// 可能編譯時出現IO錯誤。
throw new ClassNotFoundException( ie.toString() );
}
}
// 確保已經正確編譯或不需要編譯,我們開始載入原始位元組。
try {
// 讀取位元組。
byte raw[] = getBytes( classFilename );
// 轉換為類別對象
clas = defineClass( name, raw, 0, raw.length );
} catch( IOException ie ) {
// 這裡不表示失敗,可能我們處理的類別在本地類別庫中,如java.lang.Object。
}
//System.out.println( "defineClass: "+clas );
//可能在類別庫中,以預設的方式載入。
if (clas==null) {
clas = findSystemClass( name );
}
//System.out.println( "findSystemClass: "+clas );
// 如果參數resolve為true,根據需要解釋類別。
if (resolve && clas != null)
resolveClass( clas );
// 如果還沒有獲得類,表示出錯了。
if (clas == null)
throw new ClassNotFoundException( name );
// 否則,回傳這個類別物件。
return clas;
}
}
CCRun.java
一下是CCRun.java文件
import java.lang.reflect.*;
/*
CCLRun透過CompilingClassLoader載入類別來運行程式。
*/
public class CCLRun
{
static public void main( String args[] ) throws Exception {
// 第一個參數指定使用者要執行的主函數類別。
String progClass = args[0];
// 接下來的參數是傳給這個主函數類別的參數。
String progArgs[] = new String[args.length-1];
System.arraycopy( args, 1, progArgs, 0, progArgs.length );
// 建立CompilingClassLoader
CompilingClassLoader ccl = new CompilingClassLoader();
// 透過CCL載入主函數類別。
Class clas = ccl.loadClass( progClass );
// 利用反射呼叫它的主函數和傳遞參數。
// 產生一個代表主函數的參數類型的類別物件。
Class mainArgType[] = { (new String[0]).getClass() };
// 在類別中找到標準的主函數。
Method main = clas.getMethod( "main", mainArgType );
// 建立參數列表-在這裡,是一個字串陣列。
Object argsArray[] = { progArgs };
// 呼叫主函數。
main.invoke( null, argsArray );
}
}
Foo.java
以下是檔案Foo.java內容
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內容
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內容
package baz;
public class Baz
{
public Baz( String a, String b ) {
System.out.println( "baz! "+a+" "+b );
}
}
Boo.java
以下是檔案Boo.java內容
public class Boo
{
public Boo() {
System.out.println( "Boo!" );
}
}
第七部分. 總結總結透過本文你是否意識到,建立自訂類別載入器可以讓你深入Java虛擬機器的內部。你可以從任何資源加載類文件,或動態的生成它,這樣你就可以透過擴展這些功能做很多你感興趣的事,也能完成一些強大的功能。
關於ClassLoader的其它主題就像本文開頭說的,自訂類別載入器在Java嵌入式瀏覽器和小型應用程式瀏覽器中扮演著重要的