首頁  >  文章  >  Java  >  為什麼要寫ClassLoader?深層理解java中的classloader

為什麼要寫ClassLoader?深層理解java中的classloader

php是最好的语言
php是最好的语言原創
2018-07-27 09:36:402106瀏覽

什麼是ClassLoader  ?在所有的程式語言中,Java以運行在Java虛擬機器上而獨樹一格。這意味著編譯的程式將以一種獨特的,與平台無關的形式運行在目標機器上,而不是目標機器的格式。這種格式在很多方面和傳統的可執行程序相比,有很大的區別。

前言

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

  • 否則,將類別傳回給呼叫者

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!" );
  }
}

相关文章:

基于Java类的加载方式之classloader类加载器详解

Java虚拟机学习 - 类加载器(ClassLoader)

相关视频:

全面解析Java注解

以上是為什麼要寫ClassLoader?深層理解java中的classloader的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn