摘要:它們是通過來自遠(yuǎn)程的服務(wù)器的連接發(fā)送字節(jié)碼并在本地運(yùn)行,這一點(diǎn)令人興奮。中有一個(gè)自定義的,它不是從本地文件系統(tǒng)加載類文件,而是從遠(yuǎn)程服務(wù)器上獲取,通過加載原始字節(jié)碼,再在中轉(zhuǎn)化為類。它將字節(jié)碼解析為運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu),檢查其有效性等。
前言
Java ClassLoader是java運(yùn)行系統(tǒng)中一個(gè)至關(guān)重要但是經(jīng)常被忽略的組件。它負(fù)責(zé)在運(yùn)行時(shí)尋找并加載類文件。創(chuàng)建自定義的ClassLoader可以徹底重定義如何將類文件加載至系統(tǒng)。
這個(gè)教程對Java的ClassLoader進(jìn)行總體概述,并給了一個(gè)自定義ClassLoader的例子。這個(gè)ClassLoader會(huì)在加載代碼之前自動(dòng)編譯。你將會(huì)了解ClassLoader是做什么的,以及如何創(chuàng)建自定義ClassLoader。
本教程需要閱讀者對Java編程有基礎(chǔ)了解,包括創(chuàng)建,編譯和執(zhí)行簡單的命令行Java程序。
閱讀完本教程之后,你會(huì)知道如何:
擴(kuò)展JVM的功能
創(chuàng)建一個(gè)自定義的ClassLoader
學(xué)習(xí)如何將ClassLoader集成至Java應(yīng)用
修改ClassLoader使其符合Java2版本
什么是ClassLoader在所有的編程語言中,Java以運(yùn)行在Java虛擬機(jī)上而獨(dú)樹一幟。這意味著編譯的程序?qū)⒁砸环N獨(dú)特的,與平臺(tái)無關(guān)的形式運(yùn)行在目標(biāo)機(jī)器上,而不是目標(biāo)機(jī)器的格式。這種格式在很多方面和傳統(tǒng)的可執(zhí)行程序相比,有很大的區(qū)別。
Java程序與C或C++程序最大的不同在于,它不是單個(gè)可執(zhí)行文件,而是由許多多帶帶的類文件構(gòu)成,每個(gè)類文件對應(yīng)一個(gè)Java類。
不僅如此,這些類文件并不是一次性加載到內(nèi)存的,而是按需加載的。ClassLoader是JVM的一部分,它將類加載到內(nèi)存中。
此外,Java ClassLoader是用Java編寫的。這意味著可以輕松的創(chuàng)建自己的ClassLoader,無需了解JVM更多的細(xì)節(jié)。
為什么編寫ClassLoader如果JVM已經(jīng)有一個(gè)ClassLoader了,為什么還要再寫一個(gè)?好問題,默認(rèn)的ClassLoader只知道如何從本地的文件系統(tǒng)中加載類文件。一般場景下,當(dāng)你在本地編寫代碼并且在本地編譯時(shí),完全足夠了。
但是,JAVA語言最新穎的特點(diǎn)之一就是可以從本地硬盤或是互聯(lián)網(wǎng)之外的地方獲取類。比如,瀏覽器使用自定義的ClassLoader從網(wǎng)站上獲取可執(zhí)行內(nèi)容。
還有很多其它獲取類文件的方法。除了從本地或是網(wǎng)上加載類文件,還可以用類加載器來:
在執(zhí)行不受信任的代碼之前自動(dòng)驗(yàn)證數(shù)字簽名
使用用戶提供的密碼透明的解密代碼
根據(jù)用戶的特定需求創(chuàng)建自定義的動(dòng)態(tài)類
任何生成Java字節(jié)碼的內(nèi)容都可以集成到你的應(yīng)用程序中去。
自定義ClassLoader的例子如果你曾經(jīng)使用過applet,你肯定用到了一個(gè)自定義的類加載器。
在Sun發(fā)布Java語言的時(shí)候,最令人興奮的事情之一就是觀察這項(xiàng)技術(shù)是如何執(zhí)行從遠(yuǎn)程Web服務(wù)器及時(shí)加載代碼的。它們是通過來自遠(yuǎn)程的Web服務(wù)器的HTTP連接發(fā)送字節(jié)碼并在本地運(yùn)行,這一點(diǎn)令人興奮。
Java語言支持自定義ClassLoader的功能使這一想法成為可能。applet中有一個(gè)自定義的ClassLoader,它不是從本地文件系統(tǒng)加載類文件,而是從遠(yuǎn)程Web服務(wù)器上獲取,通過Http加載原始字節(jié)碼,再在jvm中轉(zhuǎn)化為類。
瀏覽器和Applet中的類加載器還有別的功能:安全管理,防止不同頁面上的applet相互影響等。
下面我們將會(huì)創(chuàng)建一個(gè)自定義的類加載器叫做CompilingClassLoader(CCL)、CCL會(huì)幫我們編譯Java代碼。它基本上就像是在運(yùn)行系統(tǒng)中直接構(gòu)建一個(gè)簡單的make程序。
ClassLoader結(jié)構(gòu)ClassLoader的基本目的是為類的請求提供服務(wù)。JVM需要一個(gè)類,于是它通過類的名字詢問ClassLoader來加載這個(gè)類。ClassLoader試著返回一個(gè)代表該類的對象。
通過覆蓋此過程不同階段對應(yīng)的方法,可以創(chuàng)建自定義的ClassLoader。
在本文的剩余部分,你會(huì)了解到ClassLoader中的一些關(guān)鍵方法。你會(huì)了解到每個(gè)方法的用途以及它在類加載過程中是如何調(diào)用的。你還會(huì)了解當(dāng)你在自定義ClassLoader時(shí)需要完成的工作。
loadClass方法##、ClassLoader.loadClass()方法是ClassLoader的入口。它的方法標(biāo)簽如下:
Class loadClass(String name, boolean resolve)
name參數(shù)代表JVM需要的類的名稱,比如Foo或是java.lang.Object。
resolve參數(shù)說明類是否需要被解析。可以把類的解析理解為完全的準(zhǔn)備好執(zhí)行類。解析并不是必要的。如果JVM只需要確定該類存在或是找出其父類,則無需解析。
在java1.1版本以前,自定義ClassLoader只需要重寫loadClass方法。
defineClass方法defineClass方法是整個(gè)ClassLoader的核心。此方法將原始字節(jié)數(shù)組轉(zhuǎn)化為一個(gè)Class對象。原始字節(jié)數(shù)組包含從本地或是遠(yuǎn)程得到的數(shù)據(jù)。
defineClass負(fù)責(zé)處理JVM的許多復(fù)雜,神秘而且依賴于具體實(shí)現(xiàn)的部分。它將字節(jié)碼解析為運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu),檢查其有效性等。不用擔(dān)心,這些你不用自己實(shí)現(xiàn)。事實(shí)上,你根本沒法重寫它,因?yàn)樵摲椒閒inal方法。
findSystemClass方法findSysetmClass方法從本地文件系統(tǒng)中加載文件。它在本地文件系統(tǒng)中查找類文件,如果存在,使用defineClass將其從原始字節(jié)轉(zhuǎn)化為類對象。這是JVM在運(yùn)行Java應(yīng)用程序時(shí)加載類的默認(rèn)機(jī)制。
對于自定義的ClassLoader,我們只會(huì)在嘗試了別的方法來加載類內(nèi)容之后,才調(diào)用findSystemClass方法。道理很簡單:自定義的ClassLoader包含加載特殊類的一些步驟,但是并非所有的類都是特殊類。比如,即便ClassLoader需要從遠(yuǎn)程網(wǎng)站上獲取一些類,還是有許多類需要從本地的Java庫中加載。這些類并不是我們關(guān)注的重點(diǎn),因此我們需要JVM用默認(rèn)的方式來獲取。
整個(gè)流程如下:
請求自定義ClassLoader加載一個(gè)類
查看遠(yuǎn)程服務(wù)器是否有該類
如果有,則獲取并返回
如果沒有,我們假設(shè)該類是位于本地的一個(gè)基礎(chǔ)類,并調(diào)用findSystemClass從文件系統(tǒng)中加載出來。
在大多數(shù)自定義的ClassLoader中,你需要先滴啊用findSystemClass來減少對遠(yuǎn)程網(wǎng)站的訪問,因?yàn)榇蠖鄶?shù)Java類都位于本地的類庫中。但是,在下一節(jié)中你會(huì)看到,在自動(dòng)將應(yīng)用代碼編譯之前,我們不希望JVM從本地文件系統(tǒng)加載類。
resolveClass方法如前文所說,類的加載是可以部分進(jìn)行(不進(jìn)行解析)或是徹底進(jìn)行的(進(jìn)行解析)。當(dāng)我們實(shí)現(xiàn)自己的loadClass方法時(shí),我們或許需要調(diào)用resolveClass方法,這取決于loadClass中的resolve參數(shù)的值。
findLoadedClass方法findLoadedClass方法充當(dāng)一個(gè)緩存調(diào)用機(jī)制:當(dāng)loadClass方法被調(diào)用時(shí),他會(huì)調(diào)用這個(gè)方法來查看類是否已經(jīng)被加載過了,省去了重復(fù)加載。這個(gè)方法應(yīng)當(dāng)最先被調(diào)用。
整合一下我們的例子中loadClass執(zhí)行以下幾步(這里我們不會(huì)特別關(guān)注到底采用了什么神奇的方法來獲取類文件。它可以是從本地,網(wǎng)絡(luò)或者是壓縮文件中獲得的,總之我們獲得了原始類文件的字節(jié)碼):
調(diào)用findLoadedClass查看是否已經(jīng)加載過該類
如果沒有,則使用神奇的魔法來獲得原始字節(jié)碼
如果獲得字節(jié)碼,調(diào)用defineClass將其轉(zhuǎn)化為Class對象
如果沒有獲得字節(jié)碼,則調(diào)用findSystemClass,看是否能從本地文件系統(tǒng)獲得類
如果resolve值為true,則調(diào)用resolveClass來解析Class對象
如果還是沒有找到類,則拋出ClassNotFoundException
否則,將類返回給調(diào)用者
CompilingClassLoaderCCL的作用是確保代碼已經(jīng)被編譯,并且是最新版本的。
以下是該類的描述:
當(dāng)需要一個(gè)類時(shí),查看該類是否在磁盤上,在當(dāng)前的目錄或是相應(yīng)的子目錄下
如果該類不存在,但是其源碼存在,在調(diào)用Java編譯器來生成類文件
如果類文件存在,查看他是否比源碼的版本舊,如果低于源碼的版本,則重新生成類文件
如果編譯失敗,或者其他的原因?qū)е聼o法從源碼中生成類文件,拋出ClassNotFoundException
如果還是沒有類文件,那么它或許在其他的一些庫中,調(diào)用findSystemClass看是否有用
如果還是找不到類,拋出ClassNotFoundException
否則,返回類
Java是如何編譯的在深入研究之前,我們應(yīng)該回過頭來看一下Java的編譯機(jī)制??偟膩碚f,當(dāng)你請求一個(gè)類的時(shí)候,Java不只是編譯各種類信息,它還編譯了別的相關(guān)聯(lián)的類。
CCL會(huì)按需一個(gè)接一個(gè)的編譯相關(guān)的類。但是,當(dāng)CCL編譯完一個(gè)類之后試著去編譯其它相關(guān)類的時(shí)候會(huì)發(fā)現(xiàn),其它的類已經(jīng)編譯完成了。為什么呢?Java編譯器遵循一個(gè)規(guī)則:如果一個(gè)類不存在,或者它相對于源碼已經(jīng)過時(shí)了,就需要編譯它。從本質(zhì)上講,Java編譯器先CCL一步完成了大部分的工作。
CCL在編譯類的時(shí)候會(huì)打印其編譯的應(yīng)用程序。在大多數(shù)場景里面,你會(huì)看到它在程序的主類上調(diào)用編譯器。
但是,有一種情況是不會(huì)在第一次調(diào)用時(shí)編譯所有類的的。如果你通過類名Class.forNasme加載一個(gè)類,Java編譯器不知道該類需要哪些信息。在這種場景下,你會(huì)看到CCL會(huì)再次運(yùn)行Java編譯器。
如何使用CompilingClassLoader為了使用CCL,我們需要用一種獨(dú)特的方式啟動(dòng)程序。正常的啟動(dòng)程序如下:
% java Foo arg1 arg2
而我們啟動(dòng)方式如下:
% java CCLRun Foo arg1 arg2
CCLRun是一個(gè)特殊的樁程序,它會(huì)創(chuàng)建一個(gè)CompilingClassLoader并使用它來加載程序的main方法,確保整個(gè)程序的類會(huì)通過CompilingClassLoader加載。CCLRun使用Java反射API來調(diào)用main方法并傳參
Java2中ClassLoader的變化Java1.2以后ClassLoader有一些變動(dòng)。原有版本的ClassLoader還是兼容的,而且在新版本下開發(fā)ClassLoader更容易了
新的版本下采用了delegate模型。ClassLoader可以將類的請求委托給父類。默認(rèn)的實(shí)現(xiàn)會(huì)先調(diào)用父類的實(shí)現(xiàn),在自己加載。但是這種模式是可以改變的。所有的ClassLoader的根節(jié)點(diǎn)是系統(tǒng)ClassLoader。它默認(rèn)會(huì)從文件系統(tǒng)中加載類。
loadClass默認(rèn)實(shí)現(xiàn)一個(gè)自定義的loadClass方法通常會(huì)嘗試用各種方法來獲得一個(gè)類的信息。如果你寫了大量的ClassLoader,你會(huì)發(fā)現(xiàn)基本上是在重復(fù)寫復(fù)雜而變化不大的代碼。
java1.2的loadClass的默認(rèn)實(shí)現(xiàn)中允許你直接重寫findClass方法,loadClass將會(huì)在合適的時(shí)候調(diào)用該方法。
這種方式的好處在于你無須重寫loadClass方法。
新方法:findClass該方法會(huì)被loadClass的默認(rèn)實(shí)現(xiàn)調(diào)用。findClass是為了包含ClassLoader所有特定的代碼,而無需寫大量重負(fù)的其他代碼
新方法:getSystenClassLoader無論你是否重寫了findClass或是loadClass方法,getSystemClassLoader允許你直接獲得系統(tǒng)的ClassLoader(而不是隱式的用findSystemClass獲得)
新方法:getParent該方法允許類加載器獲取其父類加載器,從而將請求委托給它。當(dāng)你自定義的加載器無法找到類時(shí),可以使用該方法。父類加載器是指包含創(chuàng)建該類加載代碼的加載器。
源碼// $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!" ); } }
想要了解更多開發(fā)技術(shù),面試教程以及互聯(lián)網(wǎng)公司內(nèi)推,歡迎關(guān)注我的微信公眾號(hào)!將會(huì)不定期的發(fā)放福利哦~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/76502.html
摘要:什么是為執(zhí)行字節(jié)碼提供一個(gè)運(yùn)行環(huán)境。它的實(shí)現(xiàn)主要包含三個(gè)部分,描述實(shí)現(xiàn)規(guī)格的文檔,具體實(shí)現(xiàn)和滿足要求的計(jì)算機(jī)程序以及實(shí)例具體執(zhí)行字節(jié)碼。該類先被轉(zhuǎn)化為一組字節(jié)碼并放入文件中。字節(jié)碼校驗(yàn)器通過字節(jié)碼校驗(yàn)器檢查格式并找出非法代碼。 什么是Java Development Kit (JDK)? JDK通常用來開發(fā)Java應(yīng)用和插件?;旧峡梢哉J(rèn)為是一個(gè)軟件開發(fā)環(huán)境。JDK包含Java Run...
摘要:這個(gè)例子想要說明兩個(gè)事情中以為結(jié)尾的方法將會(huì)異步執(zhí)行默認(rèn)情況下即指沒有傳入的情況下,異步執(zhí)行會(huì)使用實(shí)現(xiàn),該線程池使用一個(gè)后臺(tái)線程來執(zhí)行任務(wù)。這個(gè)例子展示了如何使用一個(gè)固定大小的線程池來實(shí)現(xiàn)大寫操作。 前言 這篇博客回顧JAVA8的CompletionStageAPI以及其在JAVA庫中的標(biāo)準(zhǔn)實(shí)現(xiàn)CompletableFuture。將會(huì)通過幾個(gè)例子來展示API的各種行為。 因?yàn)镃ompl...
摘要:什么是仿射變換一組設(shè)備無關(guān)的坐標(biāo)被用來將所有的坐標(biāo)信息傳遞給對象。對象作為對象狀態(tài)的一部分。類代表一個(gè)的仿射變化,將一組的坐標(biāo)進(jìn)行線性映射到另一組保留了平行關(guān)系和豎直關(guān)系的坐標(biāo)中。 什么是仿射變換 一組設(shè)備無關(guān)的坐標(biāo)被用來將所有的坐標(biāo)信息傳遞給Graphics2D對象。AffineTransform對象作為Graphics2D對象狀態(tài)的一部分。該對象定義了如何將用戶空間的坐標(biāo)轉(zhuǎn)化為設(shè)備...
摘要:本文簡介類概覽類構(gòu)造器總結(jié)類構(gòu)造方法類使用舉例類概覽是一個(gè)實(shí)現(xiàn)了接口,并且鍵為型的哈希表。中的條目不再被正常使用時(shí),會(huì)被自動(dòng)刪除。它的鍵值均支持。和絕大多數(shù)的集合類一樣,這個(gè)類不是同步的。 本文簡介 WeakHashMap類概覽 WeakHashMap類構(gòu)造器總結(jié) WeakHashMap類構(gòu)造方法 WeakHasjMap類使用舉例 1. WeakHashMap類概覽 Wea...
摘要:有可能一個(gè)線程中的動(dòng)作相對于另一個(gè)線程出現(xiàn)亂序。當(dāng)實(shí)際輸出取決于線程交錯(cuò)的結(jié)果時(shí),這種情況被稱為競爭條件。這里的問題在于代碼塊不是原子性的,而且實(shí)例的變化對別的線程不可見。這種不能同時(shí)在多個(gè)線程上執(zhí)行的部分被稱為關(guān)鍵部分。 為什么要額外寫一篇文章來研究volatile呢?是因?yàn)檫@可能是并發(fā)中最令人困惑以及最被誤解的結(jié)構(gòu)。我看過不少解釋volatile的博客,但是大多數(shù)要么不完整,要么難...
閱讀 1490·2021-11-15 11:38
閱讀 3631·2021-11-09 09:47
閱讀 2070·2021-09-27 13:36
閱讀 3290·2021-09-22 15:17
閱讀 2669·2021-09-13 10:27
閱讀 2916·2019-08-30 15:44
閱讀 1237·2019-08-27 10:53
閱讀 2784·2019-08-26 14:00