javaclassloaderclassnotfoundexceptionjava-17urlclassloader

How to use custom classloader for subsequent class loadings?


I have a main method that creates custom classloader and instantiates a class, called Test, with it.

public class App {
    public static void main(String[] args) throws Exception {
        try {
            Class.forName("com.mycompany.app2.Test2"); // We ensure that Test2 is not part of current classpath
            System.err.println("Should have thrown ClassNotFound");
            System.exit(1);
        } catch (ClassNotFoundException e) {
            // ignore
        }

        String jar = "C:\\experiments\\classloader-test2\\target\\classloader-test2-1.0-SNAPSHOT.jar"; // Contains Test2
        URL[] classPaths = new URL[] { new File(jar).toURI().toURL() };
        ClassLoader classLoader = new URLClassLoader(classPaths, App.class.getClassLoader());

        Thread.currentThread().setContextClassLoader(classLoader);
        Class.forName("com.mycompany.app2.Test2", true, classLoader); // Check that custom class loader can find the wanted class
        Test test = (Test) Class.forName("com.mycompany.app.Test", true, classLoader).getDeclaredConstructor().newInstance();
        test.ex(); // This throws ClassNotFound for Test2
    }
}

This class then itself instantiates another class that is not part of the original classpath, but is part of the custom one.

public class Test {
    public void ex() {
        new Test2().test();
    }
}

In my understanding of classloader, since Test was created with the custom classloader any class loadings within should be done with the same loader. But this does not seem to be the case.

Exception in thread "main" java.lang.NoClassDefFoundError: com/mycompany/app2/Test2
        at com.mycompany.app.Test.ex(Test.java:7)
        at com.mycompany.app.App.main(App.java:28)

What do I need to do in the main method to make Test#ex work, without changing Test? I'm using Java 17.


Solution

  • You create the URLClassLoader using App.class.getClassLoader() as the parent class loader. Hence, the request to load Test through the custom class loader is resolved through the parent loader, ending up at exactly the same class you’d get with Test.class in your main method.

    You could pass a different parent loader, e.g. null to denote the bootstrap loader, to forbid resolving the Test class though the parent loader but this would result in either of two unhelpful scenarios

    1. If the custom class loader has no com.mycompany.app.Test class on its own, the loading attempt would simply fail.

    2. If the custom class loader has a com.mycompany.app.Test class, i.e. inside classloader-test2-1.0-SNAPSHOT.jar, it would be a different class than the Test class referenced in your main method, loaded by the application class loader. In this case, the type cast (Test) would fail.

    In other words, the Test class referenced by you main method can not be affected by another, unrelated class loader at all.


    There is an entirely different approach which may work in some scenarios. Do not create a new class loader, when all you want to do, is to inject a new class.

    byte[] code;
    try(var is = new URL("jar:file:C:\\experiments\\classloader-test2\\target\\" +
        "classloader-test2-1.0-SNAPSHOT.jar!/com/mycompany/app2/Test2.class").openStream())
    {
      code = is.readAllBytes();
    }
    
    MethodHandles.lookup().defineClass(code);
    
    Test test = new Test();
    test.ex();
    

    This adds the class to the current class loading context, so subsequent linking will succeed. With the following catches:


    An entirely different approach to add the classes to the existing environment, would be via Java Agents, as they can add jar files to the class path.