11

I used maven to built a jar with an external classpath additions using addClasspath.

When I run that jar using java -jar artifact.jar it is able to load classes from that main jar and from all jars in the libs directory.

However if I ask the system property java.class.path it will only list the main jar. If I ask the system class loader for its urls (ClassLoader.getSystemClassLoader().getURLs()) it will also only return the main jar. If I ask any class contained in some library for its class loader it will return the system class loader.

How is the system class loader able to load those classes?

It has to have some knowledge about those libraries in order to load classes from those. Is there a way to ask it for this kind of "extended" classpath?

michas
  • 25,361
  • 15
  • 76
  • 121

2 Answers2

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.

Pace
  • 41,875
  • 13
  • 113
  • 156
4

Using reflection to access a private field in the system class loader instance presents several problems:

  • The accession can be forbidden by a security manager
  • The solution is implementation dependent

Another solution less "intrusive" is:

  1. For a given classloader enumerate all the manifest available cl.getResources("META-INF/MANIFEST.MF"). These manifests can be of jars managed by the current class loader or its ascendants classloaders.
  2. Do the same for its parent classloader
  3. Return the set of jars of those manifest in (1) but not in (2)

The only requirement for this method to work is that jars in the classpath must have a manifest in order to be returned (not much to ask).

/**
 * 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);
    }
}
idelvall
  • 1,536
  • 15
  • 25