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()方法获取类路径**。但URLClassLoaderBuiltinClassLoader*有一个共同点:它们都包装了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中,需要在模块依赖项中暴露该包,如下图所示:

导出jdk.internal.loader

现在可以编写扩展实现。本质上,我们需要调用链:

BuiltinClassLoader -> URLClassPath -> getURLs()

URLClassPath实例保存在BuiltinClassLoaderucp私有字段中,因此必须通过反射获取。这可能运行时失败,此时该怎么办?

我们将选择回退到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协同工作,因为它需要访问特定实现类,但也适用于其他供应商如ZuluGraalTemurin

模块化将暴露的JDK内部限制在单个可信模块内,防止第三方依赖的恶意访问。

本文完整源代码可在GitHub获取。


原始标题:Get Classpath From ClassLoader in Java | Baeldung