摘要:它們是通過來自遠程的服務器的連接發送字節碼并在本地運行,這一點令人興奮。中有一個自定義的,它不是從本地文件系統加載類文件,而是從遠程服務器上獲取,通過加載原始字節碼,再在中轉化為類。它將字節碼解析為運行時的數據結構,檢查其有效性等。
前言
Java ClassLoader是java運行系統中一個至關重要但是經常被忽略的組件。它負責在運行時尋找并加載類文件。創建自定義的ClassLoader可以徹底重定義如何將類文件加載至系統。
這個教程對Java的ClassLoader進行總體概述,并給了一個自定義ClassLoader的例子。這個ClassLoader會在加載代碼之前自動編譯。你將會了解ClassLoader是做什么的,以及如何創建自定義ClassLoader。
本教程需要閱讀者對Java編程有基礎了解,包括創建,編譯和執行簡單的命令行Java程序。
閱讀完本教程之后,你會知道如何:
擴展JVM的功能
創建一個自定義的ClassLoader
學習如何將ClassLoader集成至Java應用
修改ClassLoader使其符合Java2版本
什么是ClassLoader在所有的編程語言中,Java以運行在Java虛擬機上而獨樹一幟。這意味著編譯的程序將以一種獨特的,與平臺無關的形式運行在目標機器上,而不是目標機器的格式。這種格式在很多方面和傳統的可執行程序相比,有很大的區別。
Java程序與C或C++程序最大的不同在于,它不是單個可執行文件,而是由許多多帶帶的類文件構成,每個類文件對應一個Java類。
不僅如此,這些類文件并不是一次性加載到內存的,而是按需加載的。ClassLoader是JVM的一部分,它將類加載到內存中。
此外,Java ClassLoader是用Java編寫的。這意味著可以輕松的創建自己的ClassLoader,無需了解JVM更多的細節。
為什么編寫ClassLoader如果JVM已經有一個ClassLoader了,為什么還要再寫一個?好問題,默認的ClassLoader只知道如何從本地的文件系統中加載類文件。一般場景下,當你在本地編寫代碼并且在本地編譯時,完全足夠了。
但是,JAVA語言最新穎的特點之一就是可以從本地硬盤或是互聯網之外的地方獲取類。比如,瀏覽器使用自定義的ClassLoader從網站上獲取可執行內容。
還有很多其它獲取類文件的方法。除了從本地或是網上加載類文件,還可以用類加載器來:
在執行不受信任的代碼之前自動驗證數字簽名
使用用戶提供的密碼透明的解密代碼
根據用戶的特定需求創建自定義的動態類
任何生成Java字節碼的內容都可以集成到你的應用程序中去。
自定義ClassLoader的例子如果你曾經使用過applet,你肯定用到了一個自定義的類加載器。
在Sun發布Java語言的時候,最令人興奮的事情之一就是觀察這項技術是如何執行從遠程Web服務器及時加載代碼的。它們是通過來自遠程的Web服務器的HTTP連接發送字節碼并在本地運行,這一點令人興奮。
Java語言支持自定義ClassLoader的功能使這一想法成為可能。applet中有一個自定義的ClassLoader,它不是從本地文件系統加載類文件,而是從遠程Web服務器上獲取,通過Http加載原始字節碼,再在jvm中轉化為類。
瀏覽器和Applet中的類加載器還有別的功能:安全管理,防止不同頁面上的applet相互影響等。
下面我們將會創建一個自定義的類加載器叫做CompilingClassLoader(CCL)、CCL會幫我們編譯Java代碼。它基本上就像是在運行系統中直接構建一個簡單的make程序。
ClassLoader結構ClassLoader的基本目的是為類的請求提供服務。JVM需要一個類,于是它通過類的名字詢問ClassLoader來加載這個類。ClassLoader試著返回一個代表該類的對象。
通過覆蓋此過程不同階段對應的方法,可以創建自定義的ClassLoader。
在本文的剩余部分,你會了解到ClassLoader中的一些關鍵方法。你會了解到每個方法的用途以及它在類加載過程中是如何調用的。你還會了解當你在自定義ClassLoader時需要完成的工作。
loadClass方法##、ClassLoader.loadClass()方法是ClassLoader的入口。它的方法標簽如下:
Class loadClass(String name, boolean resolve)
name參數代表JVM需要的類的名稱,比如Foo或是java.lang.Object。
resolve參數說明類是否需要被解析。可以把類的解析理解為完全的準備好執行類。解析并不是必要的。如果JVM只需要確定該類存在或是找出其父類,則無需解析。
在java1.1版本以前,自定義ClassLoader只需要重寫loadClass方法。
defineClass方法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
否則,將類返回給調用者
CompilingClassLoaderCCL的作用是確保代碼已經被編譯,并且是最新版本的。
以下是該類的描述:
當需要一個類時,查看該類是否在磁盤上,在當前的目錄或是相應的子目錄下
如果該類不存在,但是其源碼存在,在調用Java編譯器來生成類文件
如果類文件存在,查看他是否比源碼的版本舊,如果低于源碼的版本,則重新生成類文件
如果編譯失敗,或者其他的原因導致無法從源碼中生成類文件,拋出ClassNotFoundException
如果還是沒有類文件,那么它或許在其他的一些庫中,調用findSystemClass看是否有用
如果還是找不到類,拋出ClassNotFoundException
否則,返回類
Java是如何編譯的在深入研究之前,我們應該回過頭來看一下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方法并傳參
Java2中ClassLoader的變化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!" ); } }
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注我的微信公眾號!將會不定期的發放福利哦~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76502.html
摘要:什么是為執行字節碼提供一個運行環境。它的實現主要包含三個部分,描述實現規格的文檔,具體實現和滿足要求的計算機程序以及實例具體執行字節碼。該類先被轉化為一組字節碼并放入文件中。字節碼校驗器通過字節碼校驗器檢查格式并找出非法代碼。 什么是Java Development Kit (JDK)? JDK通常用來開發Java應用和插件。基本上可以認為是一個軟件開發環境。JDK包含Java Run...
摘要:這個例子想要說明兩個事情中以為結尾的方法將會異步執行默認情況下即指沒有傳入的情況下,異步執行會使用實現,該線程池使用一個后臺線程來執行任務。這個例子展示了如何使用一個固定大小的線程池來實現大寫操作。 前言 這篇博客回顧JAVA8的CompletionStageAPI以及其在JAVA庫中的標準實現CompletableFuture。將會通過幾個例子來展示API的各種行為。 因為Compl...
摘要:什么是仿射變換一組設備無關的坐標被用來將所有的坐標信息傳遞給對象。對象作為對象狀態的一部分。類代表一個的仿射變化,將一組的坐標進行線性映射到另一組保留了平行關系和豎直關系的坐標中。 什么是仿射變換 一組設備無關的坐標被用來將所有的坐標信息傳遞給Graphics2D對象。AffineTransform對象作為Graphics2D對象狀態的一部分。該對象定義了如何將用戶空間的坐標轉化為設備...
摘要:本文簡介類概覽類構造器總結類構造方法類使用舉例類概覽是一個實現了接口,并且鍵為型的哈希表。中的條目不再被正常使用時,會被自動刪除。它的鍵值均支持。和絕大多數的集合類一樣,這個類不是同步的。 本文簡介 WeakHashMap類概覽 WeakHashMap類構造器總結 WeakHashMap類構造方法 WeakHasjMap類使用舉例 1. WeakHashMap類概覽 Wea...
摘要:有可能一個線程中的動作相對于另一個線程出現亂序。當實際輸出取決于線程交錯的結果時,這種情況被稱為競爭條件。這里的問題在于代碼塊不是原子性的,而且實例的變化對別的線程不可見。這種不能同時在多個線程上執行的部分被稱為關鍵部分。 為什么要額外寫一篇文章來研究volatile呢?是因為這可能是并發中最令人困惑以及最被誤解的結構。我看過不少解釋volatile的博客,但是大多數要么不完整,要么難...
閱讀 1425·2021-11-15 11:38
閱讀 3574·2021-11-09 09:47
閱讀 1972·2021-09-27 13:36
閱讀 3217·2021-09-22 15:17
閱讀 2554·2021-09-13 10:27
閱讀 2868·2019-08-30 15:44
閱讀 1172·2019-08-27 10:53
閱讀 2708·2019-08-26 14:00