Java 类加载器

Java ClassLoader是项目开发中至关重要但很少使用的组件之一。我从未在任何项目中扩展过ClassLoader。但拥有可以自定义Java类加载的自己的ClassLoader的想法令人兴奋。本文将概述Java ClassLoader,然后继续在Java中创建自定义ClassLoader。

什么是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的ClassLoader为null,这反映了引导类加载器。DNSNameService类的ClassLoader为ExtClassLoader。由于类本身在CLASSPATH中,系统类加载器加载它。
  • 当我们尝试加载HashMap时,我们的System ClassLoader将其委托给Extension ClassLoader。扩展类加载器再将其委托给Bootstrap ClassLoader。引导类加载器找到HashMap类并将其加载到JVM内存中。
  • 相同的过程也适用于DNSNameService类。但是,由于它位于$JAVA_HOME/lib/ext/dnsns.jar中,引导类加载器无法定位它。因此,它由扩展类加载器加载。
  • Blob类包含在MySql JDBC Connector jar(mysql-connector-java-5.0.7-bin.jar)中,该jar存在于项目的构建路径中。它也由System Classloader加载。
  • 由子类加载器加载的类可以看到其父类加载器加载的类。因此,由System Classloader加载的类可以看到由Extensions和Bootstrap Classloader加载的类。
  • 如果存在兄弟类加载器,则它们无法访问彼此加载的类。

为什么在Java中编写自定义类加载器?

Java默认的ClassLoader可以从本地文件系统加载类,对于大多数情况来说已经足够好了。但是,如果你期望在运行时或从FTP服务器或通过第三方网络服务在加载类的时候,就必须扩展现有的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()方法生成Class并返回。
  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类
            // 将字节数组转换为类。 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

这是我们的测试类,其中包含主函数。我们正在创建我们的ClassLoader的实例,并使用其loadClass()方法加载示例类。加载类后,我们正在使用Java Reflection 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

这些是我们的测试类,由我们的自定义类加载器加载。它们具有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自定义ClassLoader执行步骤

首先,我们将通过命令行编译所有的类。之后,我们将通过传递三个参数运行CCRun类。第一个参数是Foo类的完全分类名称,将由我们的类加载器加载。另外两个参数传递给Foo类的main函数和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的工作原理,以及如何扩展它以执行比仅仅从文件系统中获取更多的操作。

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

我们可以通过使用Java选项在JVM启动时将自定义类加载器作为默认类加载器。例如,我将在提供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