1. 概述
本文将探讨如何在现代OpenJDK发行版中从ClassLoader获取类路径(classpath)。我们将深入分析不同ClassLoader的实现机制,并提供安全可靠的解决方案。
2. ClassLoader与类路径
Java虚拟机使用*ClassLoader*在运行时解析类和其他资源。虽然ClassLoader实现可以自由选择解析机制,但类路径是标准选择。
类路径是Java程序用于通过相对命名语法定位类和其他资源的抽象概念。例如,当我们用以下命令启动Java程序时:
java -classpath /tmp/program baeldung.Main
Java运行时会在/tmp/program/baeldung/Main.class
位置搜索文件。如果文件存在且有效,JVM将加载Class并执行main方法。
程序启动后,后续的类加载将通过在目录/tmp/program
下以相对路径搜索类完成,其中类的内部名称(Internal Name)使用'/'替换'.'(例如com/example/MyClass
)。
此外,类路径还支持:
- 多个位置(使用':'分隔符)
- JAR文件
最后,我们可以指定任何有效的URL作为类路径位置,而不仅限于文件系统路径。因此,客观来说类路径等价于一组URL集合。
3. 常见ClassLoader类型
要发挥作用,ClassLoader实现必须能够通过重写以下方法解析java.lang.Class
实例:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
实现自定义ClassLoader可能很棘手。ClassLoader被设计为类似LinkedList的结构,每个ClassLoader可能有父ClassLoader,可通过getParent()
方法查询以支持委托模型。
最后,类路径和ClassLoader之间没有强制耦合关系,我们将重点关注有效使用类路径机制进行资源解析的已知类型。
3.1. java.net.URLClassLoader
URLClassLoader通过getURLs()
方法暴露其类路径,是处理动态类路径的常用选择。例如,Tomcat扩展了URLClassLoader来隔离Web应用的类路径:容器为每个部署的应用提供独立的类路径。
此外,URLClassLoader*不仅能处理本地目录,还能处理磁盘或远程(通过HTTP/FTP等协议)的JAR文件,只要它们支持URL::openConnection()*方法**。
3.2. 应用程序ClassLoader
负责启动Java程序的ClassLoader(即加载包含main方法的类并执行它的ClassLoader)称为应用程序ClassLoader。
在旧版JDK(≤9)中,应用程序ClassLoader是URLClassLoader的子类。但从OpenJDK 10开始,类层次结构发生了变化,应用程序ClassLoader继承自模块私有的jdk.internal.loader.BuiltinClassLoader
,不再是URLClassLoader的子类。
根据定义,应用程序ClassLoader的类路径绑定到-classpath
启动选项。我们可以通过查询系统属性java.class.path
在运行时获取此信息,但该属性可能被意外覆盖。若要确定实际运行时类路径,必须直接查询ClassLoader本身。
4. 从ClassLoader获取类路径
要从给定ClassLoader获取类路径,我们先定义一个ClasspathResolver接口,提供查询单个ClassLoader及其整个层次结构类路径的能力:
package com.baeldung.classloader.spi;
public interface ClasspathResolver {
void collectClasspath(ClassLoader loader, Set<URL> result);
default Set<URL> getClasspath(ClassLoader loader) {
var result = new HashSet<URL>();
collectClasspath(loader, result);
return result;
}
default Set<URL> getFullClasspath(ClassLoader loader) {
var result = new HashSet<URL>();
collectClasspath(loader, result);
loader = loader.getParent();
while (loader != null) {
collectClasspath(loader, result);
loader = loader.getParent();
}
return result;
}
}
如前所述,应用程序ClassLoader不再继承自URLClassLoader,*因此无法直接调用URLClassLoader::getURLs()方法获取类路径**。但URLClassLoader和BuiltinClassLoader*有一个共同点:它们都包装了jdk.internal.loader.URLClassPath
实例,这是实际负责定位基于URL的资源的类。
因此,有效的ClasspathResolver实现需要操作JDK中的非可见类,这要求访问未导出的包。
将JDK内部暴露给整个程序是不良实践,最好通过将ClasspathResolver接口及其实现放在模块化JAR中隔离此功能,同时定义module-info文件:
module baeldung.classloader {
exports com.baeldung.classloader.spi;
}
4.1. 基础实现
基础实现仅处理URLClassLoaders,无论程序是否可访问JDK内部都可用:
public class BasicClasspathResolver implements ClasspathResolver {
@Override
public void collectClasspath(ClassLoader loader, Set<URL> result) {
if(loader instanceof URLClassLoader ucl) {
var urls = Arrays.asList(ucl.getURLs());
result.addAll(urls);
}
}
}
4.2. 扩展实现
此实现需要访问未导出的OpenJDK类,可能遇到以下问题:
- 库使用者未设置正确的运行时标志
- 程序运行在其他可能没有BuiltinClassLoaders的JDK中
- 未来OpenJDK版本可能更改或移除BuiltinClassLoader,导致实现失效
使用内部JDK API时必须防御性编程,这意味着需要额外步骤来正确编译和运行程序。
首先,必须通过向编译器添加命令行选项来导出jdk.internal.loader
包。然后,还必须在运行时打开该包以支持反射访问来测试实现。
使用Maven作为构建和测试工具时,插件配置如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>22</source>
<target>22</target>
<compilerArgs>
<arg>--add-exports</arg>
<arg>java.base/jdk.internal.loader=baeldung.classloader</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--add-opens java.base/jdk.internal.loader=baeldung.classloader</argLine>
</configuration>
</plugin>
</plugins>
</build>
⚠️ 注意:如果不使用模块化项目,可以省略module-info文件,在导出和打开子句中将baeldung.classloader
模块替换为ALL-UNNAMED
。
某些IDE可能需要额外配置。例如在Eclipse中,需要在模块依赖项中暴露该包,如下图所示:
现在可以编写扩展实现。本质上,我们需要调用链:
BuiltinClassLoader -> URLClassPath -> getURLs()
URLClassPath实例保存在BuiltinClassLoader的ucp
私有字段中,因此必须通过反射获取。这可能运行时失败,此时该怎么办?
我们将选择回退到BasicClasspathResolver,为此需要编写一个支持类来判断是否能访问BuiltinClassLoader#ucp:
public class InternalJdkSupport {
static final Class<?> BUILT_IN_CLASSLOADER;
static final VarHandle UCP;
static {
var log = LoggerFactory.getLogger(InternalJdkSupport.class);
var version = System.getProperty("java.version");
Class<?> clazz = null;
VarHandle ucp = null;
try {
var ucpClazz = Class.forName("jdk.internal.loader.URLClassPath");
clazz = Class.forName("jdk.internal.loader.BuiltinClassLoader");
var lookup = MethodHandles.privateLookupIn(clazz, MethodHandles.lookup());
ucp = lookup.findVarHandle(clazz, "ucp", ucpClazz);
} catch (ClassNotFoundException e) {
log.warn("JDK {} 不支持 => {} 不可用", version, e.getMessage());
} catch (NoSuchFieldException e) {
log.warn("JDK {} 不支持 => BuiltinClassLoader.ucp 不存在", version);
} catch (IllegalAccessException e) {
log.warn("""
BuiltinClassLoader.ucp 需要 \
--add-opens java.base/jdk.internal.loader=baeldung.classloader
""");
}
BUILT_IN_CLASSLOADER = clazz;
UCP = ucp;
}
public static boolean available() {
return UCP != null;
}
public static Object getURLClassPath(ClassLoader loader) {
if (!isBuiltIn(loader)) {
throw new UnsupportedOperationException("Loader 不是 BuiltinClassLoader 实例");
}
if (UCP == null) {
throw new UnsupportedOperationException("""
程序必须使用以下参数初始化: \
--add-opens java.base/jdk.internal.loader=baeldung.classloader
""");
}
try {
return UCP.get(loader);
} catch (Exception e) {
throw new InternalError(e);
}
}
static boolean isBuiltIn(ClassLoader loader) {
return BUILT_IN_CLASSLOADER != null && BUILT_IN_CLASSLOADER.isInstance(loader);
}
}
有了InternalJdkSupport,能够从应用程序ClassLoader提取URL的扩展ClasspathResolver实现就变得简单:
import jdk.internal.loader.BuiltinClassLoader;
import jdk.internal.loader.URLClassPath;
public final class InternalClasspathResolver implements ClasspathResolver {
@Override
public void collectClasspath(ClassLoader loader, Set<URL> result) {
var urls = switch (loader) {
case URLClassLoader ucl -> Arrays.asList(ucl.getURLs());
case BuiltinClassLoader bcl -> {
URLClassPath ucp = (URLClassPath) InternalJdkSupport.getURLClassPath(loader);
yield ucp == null ? Collections.<URL> emptyList() : Arrays.asList(ucp.getURLs());
}
default -> {
yield Collections.<URL> emptyList();
}
};
result.addAll(urls);
}
}
4.3. 暴露ClasspathResolver
由于我们的模块只导出com.baeldung.classloader.spi
包,客户端无法直接实例化实现类。因此,应通过工厂方法提供访问:
public interface ClasspathResolver {
static ClasspathResolver get() {
if (InternalJdkSupport.available()) {
return new InternalClasspathResolver();
}
return new BasicClasspathResolver();
}
}
5. 结论
本文探讨了如何创建安全的模块化解决方案来获取常见OpenJDK ClassLoader的类路径。
扩展实现设计为与OpenJDK协同工作,因为它需要访问特定实现类,但也适用于其他供应商如Zulu、Graal和Temurin。
模块化将暴露的JDK内部限制在单个可信模块内,防止第三方依赖的恶意访问。
本文完整源代码可在GitHub获取。