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

时间:2014-09-08 16:56:31

标签: java jar classpath classloader

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

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

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

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

它必须对这些库有一些了解才能从这些库中加载类。有没有办法要求它提供这种"扩展"类路径?

2 个答案:

答案 0 :(得分:5)

The short answer is that the implementation is part of Sun's internal workings and not available through public means. getURLs() will only ever return the URLs that are passed in. There is a longer answer but it is only for the daring.

Stepping through Oracle JVM 8 with the debugger has led me through pretty much an identical structure as OpenJDK6 and you can see where it loads the class path here.

Basically, the class loader keeps a stack of URLs it has not yet parsed into memory. When asked to load a class it will pop URLs off the stack, load them as class files or jar files, and if they are jar files it will read the manifest and push class path entries onto the stack. Each time it processes a file it adds the "loader" which loaded that file to a loader map (if nothing else, to ensure it doesn't process the same file multiple times).

You can access this map if you are really motivated to do (would not recommend it) with:

        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);

Running that on a dummy setup where I have dummy-plugin.jar which references external.jar (in the manifest of dummy-plugin.jar) I get the following:

1) Immediately after creating the class loader (before loading any class):

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

2) After loading a class from dummy-plugin.jar:

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

3) After loading a class from 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}

Oddly enough this seems to fly in the face of the JDK for URLClassLoader:

The classes that are loaded are by default granted permission only to access the URLs specified when the URLClassLoader was created.

答案 1 :(得分:2)

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

  • 安全管理员可以禁止加入
  • 解决方案依赖于实现

另一种解决方案少了"侵入"是:

  1. 对于给定的类加载器,枚举所有可用的清单cl.getResources("META-INF/MANIFEST.MF")。这些清单可以是由当前类加载器或其上级类加载器管理的jar。
  2. 对其父类加载器
  3. 执行相同操作
  4. 返回(1)中显示的那些罐子,但不返回(2)
  5. 此方法的唯一要求是,类路径中的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);
        }
    }