5. 自定义 ClassLoader
内置类加载器足以满足大多数文件已经在文件系统中的情况。
然而,在需要从本地硬盘或网络加载类的场景中,我们可能需要使用自定义类加载器。
在本节中,我们将介绍自定义类加载器的一些其他用例,并演示如何创建一个。
5.1. 自定义类加载器的使用场景
自定义类加载器不仅仅是在运行时加载类很有用。一些使用场景可能包括:
-
帮助修改现有的字节码,例如编织代理
-
根据用户需求动态创建类,例如在 JDBC 中,通过动态类加载在不同驱动实现之间切换
-
在加载具有相同名称和包的类时实现类版本控制机制。这可以通过 URL 类加载器(通过 URL 加载 jar 包)或自定义类加载器来完成
下面是一些更具体的例子,说明自定义类加载器可能派上用场的地方。
例如,浏览器使用自定义类加载器从网站加载可执行内容。浏览器可以使用不同的类加载器从不同的网页加载小程序。用于运行小程序的小程序查看器包含一个 ClassLoader,它访问远程服务器上的网站而不是在本地文件系统中查找。
然后它通过 HTTP 加载原始字节码文件,并将它们转换为 JVM 内的类。即使这些小程序具有相同的名称,如果由不同的类加载器加载,它们也会被视为不同的组件。
现在我们理解了为什么自定义类加载器很重要,让我们实现一个 ClassLoader 的子类,以扩展和总结 JVM 加载类的功能。
5.2. 创建我们的自定义类加载器
为了说明,假设我们需要使用自定义类加载器从文件加载类。
我们需要扩展 ClassLoader 类并覆盖 findClass() 方法:
public class CustomClassLoader extends ClassLoader {
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
byteStream.write(nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
}
在上面的例子中,我们定义了一个自定义类加载器,它扩展了默认类加载器,并从指定文件加载字节数组。
6. 了解 java.lang.ClassLoader
让我们讨论 java.lang.ClassLoader 类的一些重要方法,以便更清楚地了解其工作原理。
6.1. loadClass() 方法
此方法负责根据给定的名称参数加载类。名称参数指的是全限定类名:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Java 虚拟机会调用 loadClass() 方法来解析类引用,并将 resolve 设置为 true。然而,并不总是需要解析类。如果我们只需要确定类是否存在,则将 resolve 参数设置为 false。
此方法作为类加载器的入口点。
该方法的默认实现按照已经讨论过的顺序搜索类。
6.2. defineClass() 方法
此方法负责将字节数组转换为类的实例。在我们使用类之前,需要对其进行解析:
protected final Class<?> defineClass(
String name, byte[] b, int off, int len) throws ClassFormatError
如果数据不包含有效的类,则抛出 ClassFormatError。
此外,我们不能覆盖此方法,因为它被标记为 final。
6.3. findClass() 方法
此方法根据全限定名称参数查找类。在遵循委派模型加载类的自定义类加载器实现中,我们需要覆盖此方法:
protected Class<?> findClass(
String name) throws ClassNotFoundException
此外,如果父类加载器找不到请求的类,loadClass() 会调用此方法。
默认实现会抛出 ClassNotFoundException,如果类加载器的任何父类加载器都找不到该类的话。
6.4. getParent() 方法
此方法返回委派的父类加载器:
public final ClassLoader getParent()
一些实现,如第 4 节中看到的,使用 null 来表示引导类加载器。
6.5. getResource() 方法
此方法尝试查找具有给定名称的资源:
public URL getResource(String name)
它首先将资源委派给父类加载器。如果父类加载器为 null,则搜索虚拟机内置类加载器的路径。
如果失败,则该方法会调用 findResource(String) 来查找资源。输入指定的资源名称可以是相对于类路径的,也可以是绝对路径。
它返回一个用于读取资源的 URL 对象,或者如果找不到资源或调用者没有足够的权限返回资源,则返回 null。
需要注意的是,Java 从类路径加载资源。
最后,在 Java 中,资源加载被认为是位置无关的,因为只要环境设置为可以找到资源,代码在何处运行并不重要。
7. 上下文类加载器(Context Class Loaders)
一般来说,上下文类加载器(Context Class Loaders)为 J2SE 引入的类加载委托方案提供了一种替代方法。
正如我们之前学到的,JVM 中的类加载器遵循分层模型,每个类加载器都有一个父类加载器,除了引导类加载器。
然而,有时当 JVM 核心类需要动态加载应用程序开发人员提供的类或资源时,我们可能会遇到问题。
例如,在 JNDI 中,核心功能由 rt.jar 中的引导类实现。但这些 JNDI 类可能会加载由独立供应商实现的 JNDI 提供者(部署在应用程序类路径中)。这种情况要求引导类加载器(父类加载器)加载对应用程序加载器(子类加载器)可见的类。
J2SE 委派在这里不起作用,为了解决这个问题,我们需要找到类加载的替代方法。这可以通过使用线程上下文加载器来实现。
java.lang.Thread 类有一个方法,getContextClassLoader(),它返回特定线程的 ContextClassLoader。当加载资源和类时,ContextClassLoader 由线程创建者提供。从 Java SE 9 开始,fork/join 公共池中的线程总是将其线程上下文类加载器设置为系统类加载器。
8. 结论
类加载器对于执行 Java 程序至关重要。在本文中,我们对它们进行了很好的介绍。
我们讨论了几种不同类型的类加载器,即引导类加载器、平台类加载器和系统类加载器。引导类加载器是它们所有类加载器的父类加载器,负责加载 JDK 内部类。而平台类加载器和系统类加载器分别从 Java 平台和类路径加载类。
我们还了解了类加载器的工作原理,并考察了一些特性,如委派、可见性和唯一性。然后我们简要解释了如何创建自定义类加载器。最后,我们介绍了上下文类加载器。