Search code examples
javajava-8jvmclassloaderjvm-arguments

Make javagent use system classloader in Java < 9


I have a java-agent which relies on some dependency libraries. We have a custom classloader which does not do globbing and provides deterministic class loading, and is specified via the -Djava.system.class.loader=. However it seems that the java-agent classes are still getting loaded via the AppClassLoader, leading to some ClassNotFoundExceptions. After having wasted a couple of hours on this, I came across this JDK bug which outlines this exact issue and is closed now, but the fix has been made only to JDK 9+.

I was wondering is there a way in Java <=8 to enforce the java-agent classes to be loaded by the classloader specified.

The custom class loader looks something like this,

public class CustomClassloader extends URLClassLoader{ 

    public CustomClassloader(ClassLoader parent) throws Exception {
         //getJarPaths has the path of the java-agent jar as well as
         //all dependency jar it needs.
        super(getJarPaths(), parent);
    }


    private static URL[] getJarPaths() throws Exception {
       //custom implementation
    }


    public void appendToClassPathForInstrumentation(String path) throws   MalformedURLException {
        assert(Thread.holdsLock(this));
        super.addURL(Paths.get(path).toUri().toURL());
    }
}

The above classloader works perfectly with JDK9+, and the javaagent is actually loaded by the custom classloader.


Solution

  • The difference between JDK 8 and JDK 9+ is that JDK 8 adds -javaagent jar to the system class path, while JDK 9+ calls appendToClassPathForInstrumentation of the custom ClassLoader instead.

    So, in JDK 8, if your custom ClassLoader delegates to the parent ClassLoader (which is system class loader) before attempting to find the class itself, the agent will be loaded by the system class loader. In order to workaround this, your custom ClassLoader may reverse delegation order: first try to find a class itself, then delegate to the parent.

    For example, this can be done by overriding loadClass method as shown below:

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    c = findClass(name);
                } catch (ClassNotFoundException e) {
                    c = super.loadClass(name, false);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    
    @Override
    public URL getResource(String name) {
        URL url = findResource(name);
        if (url != null) {
            return url;
        }
        return super.getResource(name);
    }
    
    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        List<URL> urls = Collections.list(findResources(name));
        urls.addAll(Collections.list(getParent().getResources(name)));
        return Collections.enumeration(urls);
    }