Java 类加载器
Java ClassLoader 是项目开发中至关重要但很少使用的组件之一。我从未在任何项目中扩展过 ClassLoader。但是,拥有自己的可以自定义 Java 类加载的 ClassLoader 的想法令人兴奋。本文将概述 Java ClassLoader,然后继续在 Java 中创建自定义类加载器。
什么是 Java ClassLoader?
我们知道 Java 程序运行在Java 虚拟机(JVM)上。当我们编译 Java 类时,JVM 会创建字节码,该字节码与平台和机器无关。字节码存储在.class 文件中。当我们尝试使用某个类时,ClassLoader 会将其加载到内存中。
内置类加载器类型
Java中有三种类型的内置ClassLoader。
- Bootstrap 类加载器– 加载 JDK 内部类。加载 rt.jar 和其他核心类,例如 java.lang.* 包类。
- 扩展类加载器——它从 JDK 扩展目录加载类,通常是 $JAVA_HOME/lib/ext 目录。
- 系统类加载器– 此类加载器从当前类路径加载类。我们可以在调用程序时使用 -cp 或 -classpath 命令行选项设置类路径。
类加载器层次结构
ClassLoader 在将类加载到内存中时是分层的。每当提出加载类的请求时,它都会将其委托给父类加载器。这就是在运行时环境中保持唯一性的方式。如果父类加载器找不到该类,则类加载器本身会尝试加载该类。让我们通过执行下面的 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,它反映了 Bootstrap ClassLoader。DNSNameService 类 ClassLoader 是 ExtClassLoader。由于该类本身位于 CLASSPATH 中,因此 System ClassLoader 会加载它。
- 当我们尝试加载 HashMap 时,我们的系统类加载器会将其委托给扩展类加载器。扩展类加载器会将其委托给引导类加载器。引导类加载器会找到 HashMap 类并将其加载到 JVM 内存中。
- DNSNameService 类也遵循相同的过程。但是,Bootstrap ClassLoader 无法找到它,因为它位于 中
$JAVA_HOME/lib/ext/dnsns.jar
。因此,它由 Extensions Classloader 加载。 - Blob 类包含在 MySql JDBC Connector jar (mysql-connector-java-5.0.7-bin.jar) 中,位于项目的构建路径中。它也由系统类加载器加载。
- 子类加载器加载的类对其父类加载器加载的类具有可见性。因此,系统类加载器加载的类对扩展类加载器和引导类加载器加载的类具有可见性。
- 如果存在兄弟类加载器,那么它们就不能访问彼此加载的类。
为什么要在 Java 中编写自定义 ClassLoader?
Java 默认的 ClassLoader 可以从本地文件系统加载类,这对于大多数情况来说已经足够了。但是,如果您在加载类时希望在运行时或从 FTP 服务器或通过第三方 Web 服务加载类,那么您必须扩展现有的类加载器。例如,AppletViewers 从远程 Web 服务器加载类。
Java 类加载器方法
- 当 JVM 请求一个类时,它会
loadClass()
通过传递该类的完整分类名称来调用 ClassLoader 的函数。 - loadClass() 函数调用该
findLoadedClass()
方法检查类是否已经加载。这是为了避免多次加载同一个类。 - 如果该类尚未加载,那么它将把请求委托给父 ClassLoader 来加载该类。
- 如果父 ClassLoader 找不到该类,那么它将调用 findClass() 方法在文件系统中查找该类。
Java 自定义类加载器示例
我们将通过扩展 ClassLoader 类并重写 loadClass(String name) 方法来创建我们自己的 ClassLoader。如果类名以 开头,com.journaldev
那么我们将使用我们的自定义类加载器来加载它,否则我们将调用父 ClassLoaderloadClass()
方法来加载该类。
1.CCLoader.java
这是我们的自定义类加载器,具有以下方法。
private byte[] loadClassFileData(String name)
:此方法将从文件系统读取类文件到字节数组。private Class<?> getClass(String name)
:这个方法会调用loadClassFileData()函数,并通过调用父类的defineClass()方法,生成Class并返回。public Class<?> loadClass(String name)
:此方法负责加载类。如果类名以 com.journaldev(我们的示例类)开头,则它将使用 getClass() 方法加载它,否则它将调用父级 loadClass() 函数来加载它。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 {
// This loads the byte code data from the file
b = loadClassFileData(file);
// defineClass is inherited from the ClassLoader class
// that converts byte array into a Class. defineClass is Final
// so we cannot override it
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);
// Below method is used to check that the Foo is getting loaded
// by our custom class loader i.e 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 类主函数和 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 classloader 选项后再次运行 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 示例代码。