Java类加载器深入解析:原理、机制与实践指南

这是文章《Java 类加载器》的第1部分(共3部分)。

内容片段:Java ClassLoader是项目开发中至关重要但很少被直接使用的组件之一。我从未在我的任何项目中扩展过ClassLoader。然而,拥有能够自定义Java类加载机制的自己的ClassLoader的想法令人兴奋。本文将概述Java ClassLoader的基本概念,然后继续介绍如何在Java中创建一个自定义类加载器。

Java ClassLoader是什么?

我们知道,Java程序运行在Java虚拟机(JVM)上。当我们编译一个Java类时,JVM会创建字节码,这是与平台和机器无关的中间代码。字节码存储在一个.class文件中。当我们尝试使用一个类时,类加载器负责将其加载到内存中。

内置的ClassLoader类型

Java中有三种类型的内置ClassLoader:

  1. 启动类加载器(Bootstrap ClassLoader) – 它加载JDK内部的类。它加载rt.jar和其他核心类,例如java.lang.*包中的类。
  2. 扩展类加载器(Extension ClassLoader) – 它从JDK扩展目录加载类,通常是$JAVA_HOME/lib/ext目录。
  3. 系统类加载器(System ClassLoader) – 此类加载器从当前类路径加载类。我们可以在调用程序时使用-cp或-classpath命令行选项来设置类路径。

类加载器层次结构

类加载器在将类加载到内存中时是具有层次性的。每当有一个加载类的请求时,类加载器会将其委托给父类加载器。这就是运行环境中如何保持类唯一性的方式。如果父类加载器找不到该类,那么类加载器本身会尝试加载该类。通过执行下面的Java程序来理解这个过程。

package com.Olivia.classloader;

public class ClassLoaderTest {

    public static void main(String[] args) {

        System.out.println("HashMap的类加载器: "
                + java.util.HashMap.class.getClassLoader());
        System.out.println("DNSNameService的类加载器: "
                + sun.net.spi.nameservice.dns.DNSNameService.class
                        .getClassLoader());
        System.out.println("当前类的类加载器: "
                + ClassLoaderTest.class.getClassLoader());

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

    }

}

输出:

HashMap的类加载器: null
DNSNameService的类加载器: sun.misc.Launcher$ExtClassLoader@7c354093
当前类的类加载器: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37

Java类加载器的工作原理是怎样的?

让我们从上面的程序输出中理解类加载器的工作原理。

  • java.util.HashMap的类加载器显示为null,这表示它是由启动类加载器(Bootstrap ClassLoader)加载的。DNSNameService类的类加载器是扩展类加载器(ExtClassLoader)。由于该类本身在CLASSPATH中,所以由系统类加载器(System ClassLoader)加载。
  • 当我们尝试加载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默认的类加载器可以从本地文件系统加载类,这在大多数情况下已经足够。但是,如果你希望在运行时动态加载类,或者从FTP服务器、通过第三方Web服务获取类,那么你就必须扩展现有的类加载器。例如,AppletViewers会从远程Web服务器加载类。

Java类加载器的方法

  • 当JVM请求一个类时,它会通过传递类的完全限定名来调用ClassLoader的loadClass()方法。
  • loadClass()方法调用findLoadedClass()方法来检查类是否已经被加载。这是为了避免多次加载同一个类。
  • 如果类尚未加载,那么它会将请求委托给父类加载器来加载该类。
  • 如果父类加载器找不到该类,那么它将调用findClass()方法在文件系统中查找类。

Java自定义类加载器示例

Java Custom ClassLoader Example

CCLoader.java

Java 类加载器 – 第2部分(共3部分)

自定义类加载器及其方法

这是我们的自定义类加载器,具有以下方法:

  1. loadClassFileData(String name)方法:私有方法,从文件系统中读取类文件,并将其转换为字节数组。
  2. getClass(String name)方法:调用loadClassFileData()函数,并通过调用父类的defineClass()方法来生成并返回Class对象。
  3. loadClass(String name)方法:公共方法,负责加载类。如果类名以com.Olivia(我们的示例包)开头,则使用getClass()方法加载它;否则,将调用父类的loadClass()方法来加载。
  4. CCLoader(ClassLoader parent)构造方法:负责设置父类加载器。
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
 
/**
 * 我们的自定义类加载器,用于加载类。
 * com.Olivia包中的任何类都将使用此类加载器加载。
 * 对于其他类,它将把请求委托给其父类加载器。
 */
public class CCLoader extends ClassLoader {
 
    /**
     * 此构造函数用于设置父类加载器
     */
    public CCLoader(ClassLoader parent) {
        super(parent);
    }
 
    /**
     * 从文件系统加载类。类文件应位于文件系统中。
     * 名称应该是相对于获取文件位置的路径
     *
     * @param name
     *            类的完全限定名,例如com.Olivia.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;
        }
    }
 
    /**
     * 每个类的请求都通过此方法。
     * 如果类在com.Olivia包中,我们将使用此类加载器,
     * 否则将请求委托给父类加载器。
     *
     * @param name
     *            完整类名
     */
    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        System.out.println("正在加载类 '" + name + "'");
        if (name.startsWith("com.Olivia")) {
            System.out.println("使用CCLoader加载类");
            return getClass(name);
        }
        return super.loadClass(name);
    }
 
    /**
     * 将文件(.class)读取到字节数组中。
     * 该文件应该可以作为资源访问,
     * 并确保它不在类路径中,以避免任何混淆。
     *
     * @param name
     *            文件名
     * @return 从文件读取的字节数组
     * @throws IOException
     *             如果读取文件时出现异常
     */
    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 – 测试类

这是我们的测试类,其中包含主函数。我们正在创建自定义类加载器的一个实例,并使用其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);

        // 下面的方法用于检查Foo类是否被我们的自定义类加载器CCLoader加载
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
 
}

3. Foo.java和Bar.java

这是文章《Java 类加载器》的第3部分(共3部分)。

这些是我们的测试类,它们由我们自定义的类加载器加载。它们有一个printCL()方法,用于打印类加载器信息。Foo类将由我们的自定义类加载器加载。Foo使用Bar类,因此Bar类也将由我们的自定义类加载器加载。

package com.Olivia.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.Olivia.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类的main函数和Bar构造函数。执行步骤和输出如下。

$ javac -cp . com/scdev/cl/Foo.java
$ javac -cp . com/scdev/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.Olivia.cl.Foo 1212 1313
Loading Class 'com.Olivia.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.Olivia.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$

如果您查看输出,它正在尝试加载com.Olivia.cl.Foo类。由于它继承了java.lang.Object类,所以它首先尝试加载Object类。因此,请求到达了CCLoader的loadClass方法,该方法将其委派给父类。因此,父类加载器正在加载Object、String和其他Java类。我们的类加载器仅从文件系统加载Foo和Bar类。从printCL()函数的输出中可以清楚地看出。我们可以改变loadClassFileData()功能,从FTP服务器读取字节数组,或者通过调用任何第三方服务来动态获取类字节数组。我希望本文对理解Java类加载器的工作原理以及我们如何扩展它以完成更多任务有所帮助。

将自定义的类加载器作为默认的类加载器

我们可以使用Java选项让我们的自定义类加载器成为JVM启动时的默认类加载器。例如,在提供Java类加载器选项后,我将再次运行ClassLoaderTest程序。

$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/scdev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.Olivia.classloader.ClassLoaderTest
Loading Class 'com.Olivia.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.Olivia包中。

你可以从我们的GitHub存储库中下载类加载器示例代码。

bannerAds