javaclassloaderurlclassloader

Load SPI class with URLClassLoader rise ClassNotFoundException


I did some research, But due to complexity of this situation, Not working for me.

Child first class loader and Service Provider Interface (SPI)

Like flink or tomcat, My application run as framework with platform and system classloader. Framework load plugin as module and plugin may depend some lib, so make this define:

plugin/plugin-demo.jar
depend/plugin-demo/depend-1.jar
depend/plugin-demo/depend-2.jar

framework will create two classloader like this:

URLClassLoader dependClassloader = new URLClassLoader({URI-TO-depend-jars}, currentThreadClassLoader);
URLClassLoader pluginClassloader = new URLClassLoader({URI-TO-plugin-jar},dependClassloader);

With an HelloWorld demo this is working file ( and at first I NOT set systemClassloader as parent).

But with JDBC driver com.mysql.cj.jdbc.Driver which using SPI goes into trouble:

Even I manual register driver:

Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver", true, pluginClassloader);
com.mysql.cj.jdbc.Driver driver = (com.mysql.cj.jdbc.Driver) clazz.getConstructor().newInstance();
DriverManager.registerDriver(driver);

This working fine, But after that:

DriverManager.getConnection(this.hostName, this.userName, this.password)

will rise

Caused by: java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
    at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:440)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:587)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
    ... 7 more

Or:

Caused by: java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/furryblack
    at java.sql/java.sql.DriverManager.getConnection(DriverManager.java:706)
    at java.sql/java.sql.DriverManager.getConnection(DriverManager.java:229)

I try to print all driver:

Enumeration<java.sql.Driver> driverEnumeration = DriverManager.getDrivers();
while (driverEnumeration.hasMoreElements()) {
    java.sql.Driver driver = driverEnumeration.nextElement();
    System.out.println(driver);
}

And there is no driver registered.

So, Question is: why NoClassDefFoundError ?

I have some guess: DriverManager run in systemclassloader but driver load in my classloader parent won't search in children, So I set currentThreadClassLoader as parent but still rise exception.

Update 1:

URI-TO-depend-jars is Array of File.toURI().toURL(). This design working fine with demo, So I think it should be correct.

And with debug, The ClassLoader parent chain is
ModuleLoader -> DependLoader
And with systemclassloader is
ModuleLoader -> DependLoader -> BuiltinAppClassLoader -> PlatformClassLoader -> JDKInternalLoader

This is working fine, SpecialDepend loaded

This is the full code:

Interface in jar 1:

public interface AbstractComponent {
    void handle();
}

Plugin in jar2 (depend jar3 in pom.xml):

public class Component implements AbstractComponent {

    @Override
    public void handle() {
        System.out.println("This is component handle");
        SpecialDepend.tool();
    }
}

Depend in jar3:

public class SpecialDepend {

    public static void tool() {
        System.out.println("This is tool");
    }
}

Main in jar1:

@Test
public void test() {

    String path = "D:\\Server\\Classloader";

    File libFile = Paths.get(path, "lib", "lib.jar").toFile();
    File modFile = Paths.get(path, "mod", "mod.jar").toFile();

    URLClassLoader libLoader;
    try {
        URL url;
        url = libFile.toURI().toURL();
        URL[] urls = {url};
        libLoader = new URLClassLoader(urls);
    } catch (MalformedURLException exception) {
        throw new RuntimeException(exception);
    }

    URLClassLoader modLoader;
    try {
        URL url;
        url = modFile.toURI().toURL();
        URL[] urls = {url};
        modLoader = new URLClassLoader(urls, libLoader);
    } catch (MalformedURLException exception) {
        throw new RuntimeException(exception);
    }

    try {
        Class<?> clazz = Class.forName("demo.Component", true, modLoader);
        if (AbstractComponent.class.isAssignableFrom(clazz)) {
            AbstractComponent instance = (AbstractComponent) clazz.getConstructor().newInstance();
            instance.handle();
        }
    } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException exception) {
        throw new RuntimeException(exception);
    }
}

Output is

This is component handle
This is tool

This is working perfect.

Update 2:

enter image description here

I try to print more debug and some unnecessary code, Then I found, The Driver class can be found and instancelized, But the DriverManager.registerDriver didn't register it.

So the question become: Why DriverManager can't register driver load from sub classloader?

Update3

contextClassLoader is get from Thread.currentThread().getContextClassLoader() But inject by framework with currentThread.setContextClassLoader(exclusiveClassLoader);

As double check I print the hashcode, Its same.

And I debug into DriverManager, Its was registered the driver into internal List but after that, getDrivers will got nothing.

registeredDrivers has the registered element getDrivers got no driver


Solution

  • ClassLoader looks for classes in its parent first, and the parent delegates to its parent and so on. With that said, ClassLoaders that are siblings cannot see eachothers classes.

    Also the method DriverManager#getDrivers() internally validates if the caller ClassLoader can load the class with DriverManager#isDriverAllowed(Driver, ClassLoader). this means that even if your Driver is added to the registration list, it is only added as an instance of DriverInfo, this means that it would only be loaded on demand (Lazy), and still might not register when loading is attempted, that's why you get an empty list.