【问题标题】:How to safely access the URLs of all resource files in the classpath in Java 9+?如何安全地访问 Java 9+ 中类路径中所有资源文件的 URL?
【发布时间】:2018-03-29 13:31:35
【问题描述】:

我们从Java 9 的发行说明中了解到

应用程序类加载器不再是 java.net.URLClassLoader 的实例(以前版本中从未指定的实现细节)。假设 ClassLoader::getSytemClassLoader 返回 URLClassLoader 对象的代码需要更新。

这会破坏旧代码,它会按如下方式扫描类路径:

Java

URL[] ressources = ((URLClassLoader) classLoader).getURLs();

遇到一个

java.lang.ClassCastException: 
java.base/jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to 
java.base/java.net.URLClassLoader

因此,对于 Java 9+,提出了以下解决方法作为 PR at the Apache Ignite Project,它可以在 JVM 运行时选项中进行调整:--add-opens java.base/jdk.internal.loader=ALL-UNNAMED。但是,正如下面的 cmets 所述,此 PR 从未合并到其 Master 分支中。

/*
 * Java 9 + Bridge to obtain URLs from classpath...
 */
private static URL[] getURLs(ClassLoader classLoader) {
    URL[] urls = new URL[0];

    try {
        //see https://github.com/apache/ignite/pull/2970
        Class builtinClazzLoader = Class.forName("jdk.internal.loader.BuiltinClassLoader");

        if (builtinClazzLoader != null) {
            Field ucpField = builtinClazzLoader.getDeclaredField("ucp");
            ucpField.setAccessible(true);

            Object ucpObject = ucpField.get(classLoader);
            Class clazz = Class.forName("jdk.internal.loader.URLClassPath");

            if (clazz != null && ucpObject != null) {
                Method getURLs = clazz.getMethod("getURLs");

                if (getURLs != null) {
                    urls = (URL[]) getURLs.invoke(ucpObject);
                }
            }
        }

    } catch (NoSuchMethodException | InvocationTargetException | NoSuchFieldException | IllegalAccessException | ClassNotFoundException e) {
        logger.error("Could not obtain classpath URLs in Java 9+ - Exception was:");
        logger.error(e.getLocalizedMessage(), e);
    }
    return urls;
}

但是,由于在此处使用Reflection,这会导致一些严重的头痛。这是一种反模式,受到forbidden-apis maven plugin的严厉批评:

禁止的方法调用:java.lang.reflect.AccessibleObject#setAccessible(boolean) [使用 SecurityManagers 解决访问标志的反射使用失败,并且可能不再适用于 Java 9 中的运行时类]

问题

是否有一种安全的方式来访问类/模块路径中所有资源URLs 的列表,可以由给定的类加载器访问,在OpenJDK 9/10 中不使用@ 987654334@ 导入(例如使用Unsafe)?

UPDATE(与 cmets 有关)

我知道,我可以做到

 String[] pathElements = System.getProperty("java.class.path").split(System.getProperty("path.separator"));

获取类路径中的元素,然后将它们解析为URLs。但是 - 据我所知 - 此属性仅返回应用程序启动时给出的类路径。但是,在容器环境中,这将是应用程序服务器之一,可能还不够,例如然后使用 EAR 包。

更新 2

感谢您的所有 cmets。我将测试System.getProperty("java.class.path") 是否适用于我们的 目的并更新问题,如果这满足我们的需求。

然而,似乎其他项目(可能出于其他原因,例如 Apache TomEE 8) 遭受与URLClassLoader 相关的同样痛苦 - 因此,我认为这是一个有价值的问题.

更新 3

最后,我们确实切换到 classgraph 并将我们的代码迁移到这个库,以解决我们的用例,以从类路径加载捆绑为 JAR 的 ML 资源。

【问题讨论】:

  • 如果您想要类路径元素的 URL,请查看系统属性 java.class.path。
  • 很容易拆分 java.class.path 属性的值并为每个元素创建一个文件 URL,这正是应用程序类加载器在初始化时所做的。
  • System.getProperty("java.class.path") 仍然有效,您可以轻松地为其创建 URL。但关键是,这就是类路径。模块路径可能涉及更多,没有人说它必须完全可以表示为URLs 的列表。我想,这就是不再将URLClassLoader 作为应用程序加载器的原因之一;你不应该假设你在一堆URLs 上运行。
  • 这个属性只返回应用程序启动时给出的类路径”——确切地说,因为 Java 9 将禁止修改类路径,所以这是唯一的类应用程序类加载器的路径。但是当您谈论容器时,您根本就不是在谈论应用程序加载器。只要你没有改变你的应用服务器,容器加载器可能仍然是URLClassLoaders。最后,它似乎是一个XY problem。你想对那些URLs 做什么?
  • 顺便说一下,你提到的 PR 实际上并没有合并,但是团队已经在他们的 IgniteUtils.java#classLoaderUrls() 中采用了与你类似的方法,尽管我现在已经提出了一个错误 here

标签: java classloader java-9


【解决方案1】:

我想这是an XY problem。访问类路径上所有资源的 URL 不是 Java 支持的操作,也不是一件好事。正如您在这个问题中已经看到的那样,如果您尝试这样做,您将一直与框架作斗争。将会有一百万个边缘案例会破坏您的解决方案(自定义类加载器、EE 容器等)。

请您详细说明您为什么要这样做?

如果您有某种插件系统并且正在寻找与您的代码接口的模块,这些模块可能在运行时提供,那么您应该使用the ServiceLoader API,即:

通过将提供者配置文件放置在资源目录META-INF/services 中来标识为类路径打包为JAR 文件的服务提供者。提供者配置文件的名称是服务的完全限定二进制名称。提供者配置文件包含服务提供者的完全限定二进制名称列表,每行一个。 例如,假设服务提供者com.example.impl.StandardCodecs 被打包在一个用于类路径的JAR 文件中。 JAR 文件将包含一个提供程序配置文件,名为:

META-INF/services/com.example.CodecFactory

包含该行:

com.example.impl.StandardCodecs # Standard codecs

【讨论】:

  • 感谢您的回答。我会看看你的建议。
  • +1 根本没有安全 方法可以将所有类路径源作为 URL。从 Java 1.0 开始,Java VM 已经能够从任意源加载类文件,而不限于通过 URL “可访问”的任何内容。当类名作为字符串提供时,类加载器不需要提供比能够加载类更多的功能。类路径浏览和对内部 URL 的访问将适用于某些类加载器,但在其他运行时环境中根本不可用。
  • 但是对于那些必须编写向后兼容 Java8 的代码的人来说,新的 API,例如层和服务,没有帮助。 Jorn Vernee 的 sn-p 和包装类加载器的新 URL 编码器坚持使用 java8 API,这本身就很有帮助。
  • @CanonicalChris:ServiceLoader API 是在 1.6 中添加的,并不是新的
【解决方案2】:

AFAIK 您可以解析java.class.path 系统属性以获取网址:

String classpath = System.getProperty("java.class.path");
String[] entries = classpath.split(File.pathSeparator);
URL[] result = new URL[entries.length];
for(int i = 0; i < entries.length; i++) {
    result[i] = Paths.get(entries[i]).toAbsolutePath().toUri().toURL();
}

System.out.println(Arrays.toString(result)); // e.g. [file:/J:/WS/Oxygen-Stable/jdk10/bin/]

【讨论】:

  • 这取决于环境,我猜。例如,想象一个具有不同类路径加载器视图的 TomEE 应用程序服务器。目前,除了上面的代码示例之外,我们的生产代码还使用这种方法来获得执行环境/上下文中 URL 的“更多”完整视图。
  • @rzo 我不熟悉。你的意思是在哪里使用单独的类加载器?在这种情况下,我会假设它又是一个 URLClassloader,并且可以公开 api 来获取 url。无论如何,如果这不能解决问题,您可能需要在您的问题中添加有关您的环境的额外细节。
  • 这在很多环境下是行不通的,包括像Tomcat这样的EE容器,像IntelliJ这样的IDE,像Spring Boot这样的自定义类加载器等等。请看下面我的回答。
猜你喜欢
  • 1970-01-01
  • 2021-07-29
  • 1970-01-01
  • 2010-10-26
  • 1970-01-01
  • 2015-03-06
  • 2018-07-23
  • 1970-01-01
相关资源
最近更新 更多