貓頭鷹的深夜翻譯:理解java的classloader

前言

Java ClassLoader是java運行系統中一個相當重要可是常常被忽略的組件。它負責在運行時尋找並加載類文件。建立自定義的ClassLoader能夠完全重定義如何將類文件加載至系統。java

這個教程對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
  • 不然,將類返回給調用者

CompilingClassLoader

CCL的做用是確保代碼已經被編譯,而且是最新版本的。
如下是該類的描述:

  • 當須要一個類時,查看該類是否在磁盤上,在當前的目錄或是相應的子目錄下
  • 若是該類不存在,可是其源碼存在,在調用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!" );
  }
}

clipboard.png
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~

相關文章
相關標籤/搜索