类加载器如何加载清单类路径中引用的类?

11
我使用Maven构建了一个JAR,并使用addClasspath添加了外部类路径。
当我使用java -jar artifact.jar运行该JAR时,它可以从主JAR和libs目录中的所有JAR加载类。
但是,如果我查询系统属性java.class.path,它只会列出主JAR。如果我查询系统类加载器的URL(ClassLoader.getSystemClassLoader().getURLs()),它也只会返回主JAR。如果我查询某个库中包含的任何类的类加载器,它将返回系统类加载器。
系统类加载器如何能够加载这些类?
它必须对这些库有一些了解才能从中加载类。是否有一种方法可以询问它这种“扩展”的类路径?

你能打开这个jar包并查看生成的清单文件来了解发生了什么吗? - gandaliter
清单文件中有一个类路径条目,列出了libs目录中的所有jar包。- 就像预期的那样。 - michas
2个回答

5
简短的回答是,实现部分属于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的虚拟设置中运行,我获得以下结果:

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) 从外部.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}

奇怪的是,这似乎与URLClassLoader的JDK相矛盾:

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


不错的黑客技巧!但在安全管理器存在的情况下可能会出现问题。 - idelvall

4
使用反射访问系统类加载器实例中的私有字段存在几个问题:
  • 访问可能会被安全管理器禁止
  • 解决方案取决于实现

另一种不太“侵入式”的解决方案是:

  1. 对于给定的类加载器,枚举所有可用的清单 cl.getResources("META-INF/MANIFEST.MF")。这些清单可以是当前类加载器或其祖先类加载器管理的jar包的清单。
  2. 对其父类加载器执行相同操作
  3. 返回 (1) 中那些manifest中的jar但不在 (2) 中manifest中的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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接