【问题标题】:How does a classloader load classes reference in the manifest classpath?类加载器如何在清单类路径中加载类引用?
【发布时间】:2014-11-01 23:18:55
【问题描述】:

我使用 maven 使用 addClasspath 构建了一个带有外部类路径添加的 jar。

当我使用 java -jar artifact.jar 运行该 jar 时,它能够从该主 jar 和 libs 目录中的所有 jar 加载类。

但是,如果我询问系统属性java.class.path,它只会列出主 jar。如果我向系统类加载器询问其 url (ClassLoader.getSystemClassLoader().getURLs()),它也只会返回主 jar。如果我向某个库中包含的任何类询问其类加载器,它将返回系统类加载器。

系统类加载器如何加载这些类?

它必须对这些库有一些了解才能从这些库中加载类。有没有办法向它询问这种“扩展”类路径?

【问题讨论】:

  • 你能打开 jar 看看生成的清单看看发生了什么吗?
  • 清单有一个类路径条目,其中列出了 libs 目录中的所有 jar。 - 完全符合预期。

标签: java jar classpath classloader


【解决方案1】:

简短的回答是,该实施是 Sun 内部工作的一部分,无法通过公共方式获得。 getURLs() 只会返回传入的 URL。有更长的答案,但它只适用于大胆的人。

使用调试器单步执行 Oracle JVM 8 使我了解了与 OpenJDK6 几乎相同的结构,您可以看到它加载类路径 here 的位置。

基本上,类加载器会保留一堆尚未解析到内存中的 URL。当被要求加载一个类时,它将从堆栈中弹出 URL,将它们作为类文件或 jar 文件加载,如果它们是 jar 文件,它将读取清单并将类路径条目推送到堆栈上。每次它处理一个文件时,它都会将加载该文件的“加载器”添加到加载器映射中(如果没有别的,以确保它不会多次处理同一个文件)。

如果您真的有动力(不推荐)这样做,您可以访问此地图:

        Field secretField = URLClassLoader.class.getDeclaredField("ucp");
        secretField.setAccessible(true);
        Object ucp = secretField.get(loader);
        secretField = ucp.getClass().getDeclaredField("lmap");
        secretField.setAccessible(true);
        return secretField.get(ucp);

在我有引用 external.jar 的 dummy-plugin.jar 的虚拟设置上运行它(在 dummy-plugin.jar 的清单中),我得到以下信息:

1) 在创建类加载器之后(在加载任何类之前):

urlClassLoader.getURLs()=[file:.../dummy-plugin.jar]
getSecretUrlsStack=[file:.../dummy-plugin.jar]
getSecretLmapField={}

2) 从 dummy-plugin.jar 加载类后:

urlClassLoader.getURLs()=[file:.../dummy-plugin.jar]
getSecretUrlsStack=[file:.../external.jar]
getSecretLmapField={file:.../dummy-plugin.jar=sun.misc.URLClassPath$JarLoader@736e9adb}

3) 从 external.jar 加载类后:

urlClassLoader.getURLs()=[file:.../dummy-plugin.jar]
getSecretUrlsStack=[]
getSecretLmapField={file:.../dummy-plugin.jar=sun.misc.URLClassPath$JarLoader@736e9adb, file:.../external.jar=sun.misc.URLClassPath$JarLoader@2d8e6db6}

奇怪的是,这似乎在JDK for URLClassLoader面前飞了起来:

默认情况下,加载的类仅授予 访问创建 URLClassLoader 时指定的 URL。

【讨论】:

  • 不错的技巧!但在安全管理员在场的情况下可能会出现问题
【解决方案2】:

使用反射访问系统类加载器实例中的私有字段存在几个问题:

  • 安全管理员可以禁止访问
  • 解决方案取决于实现

另一个不那么“侵入性”的解决方案是:

  1. 对于给定的类加载器,枚举所有可用的清单cl.getResources("META-INF/MANIFEST.MF")。这些清单可以是由当前类加载器或其上层类加载器管理的 jar。
  2. 对其父类加载器执行相同操作
  3. 返回在 (1) 中但不在 (2) 中的清单的 jar 集

此方法工作的唯一要求是类路径中的 jar 必须具有清单才能返回(要求不多)。

/**
 * Returns the search path of URLs for loading classes and resources for the 
 * specified class loader, including those referenced in the 
 * {@code Class-path} header of the manifest of a executable jar, in the 
 * case of class loader being the system class loader. 
 * <p>
 * Note: These last jars are not returned by 
 * {@link java.net.URLClassLoader#getURLs()}.
 * </p>
 * @param cl
 * @return 
 */
public static URL[] getURLs(URLClassLoader cl) {
    if (cl.getParent() == null || !(cl.getParent() 
            instanceof URLClassLoader)) {
        return cl.getURLs();
    }
    Set<URL> urlSet = new LinkedHashSet();
    URL[] urLs = cl.getURLs();
    URL[] urlsFromManifest = getJarUrlsFromManifests(cl);
    URLClassLoader parentCl = (URLClassLoader) cl.getParent();
    URL[] ancestorUrls = getJarUrlsFromManifests(parentCl);

    for (int i = 0; i < urlsFromManifest.length; i++) {
        urlSet.add(urlsFromManifest[i]);
    }
    for (int i = 0; i < ancestorUrls.length; i++) {
        urlSet.remove(ancestorUrls[i]);
    }
    for (int i = 0; i < urLs.length; i++) {
        urlSet.add(urLs[i]);
    }
    return urlSet.toArray(new URL[urlSet.size()]);
}

/**
 * Returns the URLs of those jar managed by this classloader (or its 
 * ascendant classloaders) that have a manifest
 * @param cl
 * @return 
 */
private static URL[] getJarUrlsFromManifests(ClassLoader cl) {
    try {
        Set<URL> urlSet = new LinkedHashSet();
        Enumeration<URL> manifestUrls = 
                cl.getResources("META-INF/MANIFEST.MF");
        while (manifestUrls.hasMoreElements()) {
            try {
                URL manifestUrl = manifestUrls.nextElement();
                if(manifestUrl.getProtocol().equals("jar")) {
                    urlSet.add(new URL(manifestUrl.getFile().substring(0, 
                            manifestUrl.getFile().lastIndexOf("!"))));
                }
            } catch (MalformedURLException ex) {
                throw new AssertionError();
            }
        }
        return urlSet.toArray(new URL[urlSet.size()]);
    } catch (IOException ex) {
        throw new RuntimeException(ex);
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-21
    • 1970-01-01
    • 1970-01-01
    • 2011-01-26
    • 2021-04-21
    相关资源
    最近更新 更多