Java ClassLoader

Java ClassLoader是項目開發中關鍵但很少使用的組件之一。我從未在任何項目中擴展過ClassLoader。但是,擁有自己的ClassLoader,可以自定義Java類的加載,這個想法很令人興奮。本文將概述Java ClassLoader,然後介紹如何在Java中創建自定義的類加載器。

什麼是Java ClassLoader?

我們知道Java程序在Java虛擬機(JVM)上運行。當我們編譯一個Java類時,JVM會創建字節碼,這是與平台和機器無關的。字節碼存儲在一個.class文件中。當我們嘗試使用一個類時,ClassLoader將其加載到內存中。

內置ClassLoader類型

Java中有三種內置的ClassLoader類型。

  1. 引導ClassLoader – 它加載JDK內部類。它加載rt.jar和其他核心類,例如java.lang.*包中的類。
  2. 擴展ClassLoader – 它從JDK擴展目錄(通常是$JAVA_HOME/lib/ext目錄)加載類。
  3. 系統類別加載器 – 這個類別加載器從當前的類路徑加載類。我們可以在調用程序時使用 -cp 或 -classpath 命令行選項來設置類路徑。

類加載器層次結構

類加載器在將類加載到內存中時是具有層次結構的。每當有請求加載一個類時,它將其委派給父類加載器。這就是在運行時環境中維護唯一性的方式。如果父類加載器找不到該類,那麼類加載器本身將嘗試加載該類。通過執行下面的Java程序來理解這一點。

package com.journaldev.classloader;

public class ClassLoaderTest {

    public static void main(String[] args) {

        System.out.println("class loader for HashMap: "
                + java.util.HashMap.class.getClassLoader());
        System.out.println("class loader for DNSNameService: "
                + sun.net.spi.nameservice.dns.DNSNameService.class
                        .getClassLoader());
        System.out.println("class loader for this class: "
                + ClassLoaderTest.class.getClassLoader());

        System.out.println(com.mysql.jdbc.Blob.class.getClassLoader());

    }

}

輸出:

class loader for HashMap: null
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@7c354093
class loader for this class: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37

Java類加載器的工作原理?

讓我們從上面的程序輸出中了解類加載器的工作原理。

  • 類加載器 java.util.HashMap 的類加載器為空,這反映了引導類加載器。DNSNameService 類的類加載器為 ExtClassLoader。由於類本身在 CLASSPATH 中,系統類加載器加載它。
  • 當我們嘗試加載HashMap時,我們的系統類加載器將其委託給擴展類加載器。擴展類加載器將其委託給啟動類加載器。啟動類加載器找到HashMap類並將其加載到JVM內存中。
  • 對於DNSNameService類,同樣的過程也被遵循。但是,由於它在$JAVA_HOME/lib/ext/dnsns.jar中,啟動類加載器無法找到它。因此,它由擴展類加載器加載。
  • Blob類包含在MySql JDBC Connector jar(mysql-connector-java-5.0.7-bin.jar)中,該jar存在於項目的構建路徑中。它也由系統類加載器加載。
  • 由子類加載器加載的類可以看到由其父類加載器加載的類。因此,由系統類加載器加載的類可以看到由擴展和啟動類加載器加載的類。
  • 如果有同級類加載器,則它們無法訪問彼此加載的類。

為什麼在Java中編寫自定義類加載器?

Java默認的ClassLoader可以從本地文件系統加載類,這對大多數情況已經足夠。但是,如果你期望在運行時從FTP服務器或通過第三方Web服務器加載類,那麼你必須擴展現有的ClassLoader。例如,AppletViewers從遠程Web服務器加載類。

Java ClassLoader方法

  • 當JVM請求一個類時,它通過傳遞類的完整分類名調用ClassLoader的loadClass()函數。
  • loadClass()函數調用findLoadedClass()方法檢查類是否已經加載。這是為了避免多次加載相同的類。
  • 如果該類尚未加載,則它將將請求委派給父ClassLoader以加載該類。
  • 如果父ClassLoader找不到該類,則它將調用findClass()方法在文件系統中查找類。

Java自定義ClassLoader示例

我們將通過擴展ClassLoader類並覆蓋loadClass(String name)方法來創建自己的ClassLoader。如果類名以com.journaldev開頭,那麼我們將使用我們的自定義類加載器加載它,否則我們將調用父ClassLoader的loadClass()方法來加載該類。

1. CCLoader.java

這是我們的自定義類加載器,下面是其中的方法。

  1. private byte[] loadClassFileData(String name):該方法將從文件系統中讀取類文件並轉換為字節數組。
  2. private Class<?> getClass(String name):該方法將調用loadClassFileData()函數,通過調用父類的defineClass()方法來生成類並返回它。
  3. public Class<?> loadClass(String name):該方法負責加載類。如果類名以com.journaldev開頭(我們的示例類),則它將使用getClass()方法加載,否則將調用父loadClass()函數進行加載。
  4. public CCLoader(ClassLoader parent):這是構造函數,負責設置父ClassLoader。
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
 
/**
 * Our Custom ClassLoader to load the classes. Any class in the com.journaldev
 * package will be loaded using this ClassLoader. For other classes, it will delegate the request to its Parent ClassLoader.
 *
 */
public class CCLoader extends ClassLoader {
 
    /**
     * This constructor is used to set the parent ClassLoader
     */
    public CCLoader(ClassLoader parent) {
        super(parent);
    }
 
    /**
     * Loads the class from the file system. The class file should be located in
     * the file system. The name should be relative to get the file location
     *
     * @param name
     *            Fully Classified name of the class, for example, com.journaldev.Foo
     */
    private Class getClass(String name) throws ClassNotFoundException {
        String file = name.replace('.', File.separatorChar) + ".class";
        byte[] b = null;
        try {
            // 從文件加載字節碼數據
            b = loadClassFileData(file);
            // defineClass 繼承自ClassLoader類
            // 將字節數組轉換為Class的方法。 defineClass 是 Final
            // 因此我們無法覆蓋它
            Class c = defineClass(name, b, 0, b.length);
            resolveClass(c);
            return c;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    /**
     * Every request for a class passes through this method. If the class is in
     * com.journaldev package, we will use this classloader or else delegate the
     * request to parent classloader.
     *
     *
     * @param name
     *            Full class name
     */
    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        System.out.println("Loading Class '" + name + "'");
        if (name.startsWith("com.journaldev")) {
            System.out.println("Loading Class using CCLoader");
            return getClass(name);
        }
        return super.loadClass(name);
    }
 
    /**
     * Reads the file (.class) into a byte array. The file should be
     * accessible as a resource and make sure that it's not in Classpath to avoid
     * any confusion.
     *
     * @param name
     *            Filename
     * @return Byte array read from the file
     * @throws IOException
     *             if an exception comes in reading the file
     */
    private byte[] loadClassFileData(String name) throws IOException {
        InputStream stream = getClass().getClassLoader().getResourceAsStream(
                name);
        int size = stream.available();
        byte buff[] = new byte[size];
        DataInputStream in = new DataInputStream(stream);
        in.readFully(buff);
        in.close();
        return buff;
    }
}

2. CCRun.java

這是我們的測試類,其中包含main 函數。我們創建了我們的ClassLoader的一個實例,並使用它的loadClass()方法加載示例類。 加載類後,我們使用Java反射API來調用它的方法。

import java.lang.reflect.Method;
 
public class CCRun {
 
    public static void main(String args[]) throws Exception {
        String progClass = args[0];
        String progArgs[] = new String[args.length - 1];
        System.arraycopy(args, 1, progArgs, 0, progArgs.length);

        CCLoader ccl = new CCLoader(CCRun.class.getClassLoader());
        Class clas = ccl.loadClass(progClass);
        Class mainArgType[] = { (new String[0]).getClass() };
        Method main = clas.getMethod("main", mainArgType);
        Object argsArray[] = { progArgs };
        main.invoke(null, argsArray);

        // 以下方法用於檢查是否已通過我們的自定義類加載器即CCLoader加載Foo
        // 
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
 
}

3. Foo.java 和 Bar.java

這些是我們的測試類,它們由我們的自定義類加載器加載。 它們具有printCL()方法,該方法用於打印ClassLoader信息。 Foo類將由我們的自定義類加載器加載。 Foo 使用Bar類,因此Bar類也將由我們的自定義類加載器加載。

package com.journaldev.cl;
 
public class Foo {
    static public void main(String args[]) throws Exception {
        System.out.println("Foo Constructor >>> " + args[0] + " " + args[1]);
        Bar bar = new Bar(args[0], args[1]);
        bar.printCL();
    }
 
    public static void printCL() {
        System.out.println("Foo ClassLoader: "+Foo.class.getClassLoader());
    }
}
package com.journaldev.cl;
 
public class Bar {
 
    public Bar(String a, String b) {
        System.out.println("Bar Constructor >>> " + a + " " + b);
    }
 
    public void printCL() {
        System.out.println("Bar ClassLoader: "+Bar.class.getClassLoader());
    }
}

4. Java 自定義類別加載器執行步驟

首先,我們將通過命令行編譯所有類別。然後,我們將通過傳遞三個參數運行 CCRun 類。第一個參數是 Foo 類的完全分類名稱,將由我們的類加載器加載。其他兩個參數將傳遞給 Foo 類的主函數和 Bar 構造函數。執行步驟和輸出如下。

$ javac -cp . com/journaldev/cl/Foo.java
$ javac -cp . com/journaldev/cl/Bar.java
$ javac CCLoader.java
$ javac CCRun.java
CCRun.java:18: warning: non-varargs call of varargs method with inexact argument type for last parameter;
cast to java.lang.Class<?> for a varargs call
cast to java.lang.Class<?>[] for a non-varargs call and to suppress this warning
Method printCL = clas.getMethod("printCL", null);
^
1 warning
$ java CCRun com.journaldev.cl.Foo 1212 1313
Loading Class 'com.journaldev.cl.Foo'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.Exception'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.io.PrintStream'
Foo Constructor >>> 1212 1313
Loading Class 'com.journaldev.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$

如果您查看輸出,它正在嘗試加載 com.journaldev.cl.Foo 類。由於它擴展了 java.lang.Object 類,它首先嘗試加載 Object 類。因此,請求到達 CCLoader loadClass 方法,該方法將其委派給父類。因此,父類加載器正在從文件系統加載 Object、String 和其他 java 類。我們的 ClassLoader 只從文件系統加載 Foo 和 Bar 類。這清楚地表現在 printCL() 函數的輸出中。我們可以更改 loadClassFileData() 功能,從 FTP 服務器讀取字节数組,或通過調用任何第三方服務來即時獲取類字节数組。我希望這篇文章對於理解 Java ClassLoader 的工作原理以及我們如何擴展它以執行更多操作而不僅僅是從文件系統中取得它會有所幫助。

將自定義ClassLoader設置為默認ClassLoader

我們可以在JVM啟動時使用Java選項將自定義類加載器設置為默認類加載器。例如,在提供java類加載器選項後,我將再次運行ClassLoaderTest程序。

$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/journaldev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.journaldev.classloader.ClassLoaderTest
Loading Class 'com.journaldev.classloader.ClassLoaderTest'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.util.HashMap'
Loading Class 'java.lang.Class'
Loading Class 'java.io.PrintStream'
class loader for HashMap: null
Loading Class 'sun.net.spi.nameservice.dns.DNSNameService'
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@24480457
class loader for this class: CCLoader@38503429
Loading Class 'com.mysql.jdbc.Blob'
sun.misc.Launcher$AppClassLoader@2f94ca6c
$

CCLoader正在加載ClassLoaderTest類,因為它在com.journaldev包中。

您可以從我們的GitHub存儲庫中下載ClassLoader示例代碼。

Source:
https://www.digitalocean.com/community/tutorials/java-classloader