什麼是ClassLoader ?在所有的程式語言中,Java以運行在Java虛擬機器上而獨樹一格。這意味著編譯的程式將以一種獨特的,與平台無關的形式運行在目標機器上,而不是目標機器的格式。這種格式在很多方面和傳統的可執行程序相比,有很大的區別。
Java ClassLoader是java運作系統中一個至關重要但是經常被忽略的元件。它負責在運行時尋找並載入類別文件。建立自訂的ClassLoader可以徹底重訂如何將類別檔案載入至系統。
這個教學對Java的ClassLoader進行整體概述,並給了一個自訂ClassLoader的例子。這個ClassLoader會在載入程式碼之前自動編譯。你將會了解ClassLoader是做什麼的,以及如何建立自訂ClassLoader。
本教學需要閱讀者對Java程式設計有基礎了解,包括創建,編譯和執行簡單的命令列Java程式。
閱讀完本教學之後,你會知道如何:
擴充JVM的功能
建立一個自訂的ClassLoader
學習如何將ClassLoader整合到Java應用程式
修改ClassLoader使其符合Java2版本
自訂ClassLoader的範例
在Sun發布Java語言的時候,最令人興奮的事情之一就是觀察這項技術是如何執行從遠端Web伺服器及時載入程式碼的。它們是透過來自遠端的Web伺服器的HTTP連接發送字節碼並在本地運行,這一點令人興奮。
瀏覽器和Applet中的類別載入器還有別的功能:安全管理,防止不同頁面上的applet相互影響等。
下面我們將會建立一個自訂的類別載入器叫做
CompilingClassLoader(CCL)、CCL會幫我們編譯Java程式碼。它基本上就像是在運行系統中直接建立一個簡單的make程式。
Class loadClass(String name, boolean resolve)######name###參數代表JVM所需的類別的名稱,例如###Foo###或是###java.lang.Object### 。 ###
resolve
參數說明類別是否需要被解析。可以把類別的解析理解為完全的準備好執行類別。解析並不是必要的。如果JVM只需要確定該類別存在或是找出其父類,則無需解析。
在java1.1版本以前,自訂ClassLoader只需要重寫loadClass
方法。
defineClass
方法是整個ClassLoader的核心。此方法將原始位元組陣列轉換為一個Class
物件。原始位元組數組包含從本地或遠端得到的資料。
defineClass
負責處理JVM的許多複雜,神秘而且依賴於具體實現的部分。它將字節碼解析為運行時的資料結構,檢查其有效性等。不用擔心,這些你不用自己實現。事實上,你根本無法重寫它,因為該方法是final方法。
findSystemClass方法
findSysetmClass
方法從本機檔案系統載入檔案。它在本機檔案系統中尋找類別文件,如果存在,使用defineClass
將其從原始位元組轉換為類別物件。這是JVM在執行Java應用程式時載入類別的預設機制。
對於自訂的ClassLoader,我們只會在嘗試了別的方法來載入類別內容之後,才呼叫findSystemClass
方法。道理很簡單:自訂的ClassLoader包含一些載入特殊類別的步驟,但並非所有的類別都是特殊類別。例如,即便ClassLoader需要從遠端網站取得一些類別,還是有許多類別需要從本地的Java庫載入。這些類別並不是我們關注的重點,因此我們需要JVM以預設的方式來取得。
整個流程如下:
請求自訂ClassLoader載入一個類別
查看遠端伺服器是否有該類別
如果有,則獲取並返回
如果沒有,我們假設該類是位於本地的基礎類,並調用findSystemClass
從檔案系統載入出來。
在大多數自訂的ClassLoader中,你需要先滴啊用findSystemClass
來減少對遠端網站的訪問,因為大多數Java類別都位於本地的類別庫中。但是,在下一節你會看到,在自動將應用程式碼編譯之前,我們不希望JVM從本機檔案系統載入類別。
resolveClass
方法如前文所說,類別的載入是可以部分進行(不進行解析)或是徹底進行的(進行解析)。當我們實作自己的loadClass
方法時,我們或許需要呼叫resolveClass
方法,這取決於loadClass
中的resolve
參數的值。
findLoadedClass
方法findLoadedClass
方法充當快取呼叫機制:當loadClass
方法被呼叫時,他會呼叫這個方法來查看類別是否已經載入過了,省去了重複載入。這個方法應先被呼叫。
我們的例子中loadClass
執行以下幾步(這裡我們不會特別關注到底採用了什麼神奇的方法來取得類別檔案。它可以是從本地,網路或者是壓縮檔案中獲得的,總之我們獲得了原始類別檔案的字節碼):
呼叫findLoadedClass
查看是否已經載入過該類別
如果沒有,則使用神奇的魔法來獲得原始字節碼
如果獲得字節碼,則呼叫defineClass
將其轉換為Class
物件
如果沒有取得字節碼,則呼叫findSystemClass
,看是否能從本機檔案系統取得類別
如果resolve
值為true,則呼叫resolveClass
來解析Class
物件
如果還是沒有找到類,則拋出ClassNotFoundException
否則,將類別傳回給呼叫者
CompilingClassLoader
CCL
的作用是確保程式碼已經被編譯,並且是最新的版本。
以下是該類別的描述:
當需要一個類別時,請查看該類別是否在磁碟上,在目前的目錄或是對應的子目錄下
如果該類別不存在,但其原始碼存在,在呼叫Java編譯器來產生類別檔案
如果類別檔案存在,請查看他是否比原始碼的版本舊,如果低於原始碼的版本,則重新產生類別文件
如果編譯失敗,或者其他的原因導致無法從原始碼中產生類別文件,拋出ClassNotFoundException
如果還是沒有類別文件,那麼它或許在其他的一些函式庫中,呼叫findSystemClass
看是否有用
#如果還是找不到類,拋出ClassNotFoundException
#否則,回傳類別
在深入研究之前,我们应该回过头来看一下Java的编译机制。总的来说,当你请求一个类的时候,Java不只是编译各种类信息,它还编译了别的相关联的类。
CCL会按需一个接一个的编译相关的类。但是,当CCL编译完一个类之后试着去编译其它相关类的时候会发现,其它的类已经编译完成了。为什么呢?Java编译器遵循一个规则:如果一个类不存在,或者它相对于源码已经过时了,就需要编译它。从本质上讲,Java编译器先CCL一步完成了大部分的工作。
CCL在编译类的时候会打印其编译的应用程序。在大多数场景里面,你会看到它在程序的主类上调用编译器。
但是,有一种情况是不会在第一次调用时编译所有类的的。如果你通过类名Class.forNasme
加载一个类,Java编译器不知道该类需要哪些信息。在这种场景下,你会看到CCL会再次运行Java编译器。
CompilingClassLoader
为了使用CCL,我们需要用一种独特的方式启动程序。正常的启动程序如下:
% java Foo arg1 arg2
而我们启动方式如下:
% java CCLRun Foo arg1 arg2
CCLRun是一个特殊的桩程序,它会创建一个CompilingClassLoader并使用它来加载程序的main方法,确保整个程序的类会通过CompilingClassLoader加载。CCLRun使用Java反射API来调用main方法并传参
Java1.2以后ClassLoader有一些变动。原有版本的ClassLoader还是兼容的,而且在新版本下开发ClassLoader更容易了
新的版本下采用了delegate模型。ClassLoader可以将类的请求委托给父类。默认的实现会先调用父类的实现,在自己加载。但是这种模式是可以改变的。所有的ClassLoader的根节点是系统ClassLoader。它默认会从文件系统中加载类。
loadClass
默认实现一个自定义的loadClass
方法通常会尝试用各种方法来获得一个类的信息。如果你写了大量的ClassLoader,你会发现基本上是在重复写复杂而变化不大的代码。
java1.2的loadClass
的默认实现中允许你直接重写findClass
方法,loadClass
将会在合适的时候调用该方法。
这种方式的好处在于你无须重写loadClass
方法。
findClass
该方法会被loadClass
的默认实现调用。findClass
是为了包含ClassLoader所有特定的代码,而无需写大量重负的其他代码
getSystenClassLoader
无论你是否重写了findClass
或是loadClass
方法,getSystemClassLoader
允许你直接获得系统的ClassLoader(而不是隐式的用findSystemClass
获得)
getParent
该方法允许类加载器获取其父类加载器,从而将请求委托给它。当你自定义的加载器无法找到类时,可以使用该方法。父类加载器是指包含创建该类加载代码的加载器。
// $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; } }
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 ); } }
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] ); } }
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(); } } }
package baz; public class Baz { public Baz( String a, String b ) { System.out.println( "baz! "+a+" "+b ); } }
public class Boo { public Boo() { System.out.println( "Boo!" ); } }
相关文章:
基于Java类的加载方式之classloader类加载器详解
相关视频:
以上是為什麼要寫ClassLoader?深層理解java中的classloader的詳細內容。更多資訊請關注PHP中文網其他相關文章!