使用 URLClassLoader 加载 SPI class 引发 ClassNotFoundException

Load SPI class with URLClassLoader rise ClassNotFoundException

我做了一些研究,但由于这种情况的复杂性,不适合我。

像 flink 或 tomcat,我的应用程序 运行 作为具有平台和系统 classloader 的框架。 框架加载插件作为模块和插件可能依赖于一些库,所以定义:

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

框架将创建两个 classloader,如下所示:

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

对于 HelloWorld 演示,这是工作文件(起初我没有将 systemClassloader 设置为父级)。

但是使用 JDBC 驱动程序 com.mysql.cj.jdbc.Driver 使用 SPI 会遇到麻烦:

偶我手动注册驱动:

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

这工作正常,但之后:

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

会上涨

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

或者:

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)

我尝试打印所有驱动程序:

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

并且没有驱动注册。

所以,问题是:为什么是 NoClassDefFoundError?

我有一些猜测:DriverManager 运行 在 systemclassloader 但我的 classloader parent 中的驱动程序加载不会在 children 中搜索,所以我将 currentThreadClassLoader 设置为 parent 但是还是涨异常。

更新 1:

URI-TO-depend-jars 是 File.toURI().toURL() 的数组。 这个设计用demo做的很好,所以我觉得应该是正确的。

并且通过调试,ClassLoader 父链是
ModuleLoader -> DependLoader
系统class加载器是
ModuleLoader -> DependLoader -> BuiltinAppClassLoader -> PlatformClassLoader -> JDKInternalLoader

这是完整代码:

jar 1 中的接口:

public interface AbstractComponent {
    void handle();
}

jar2 中的插件(依赖 pom.xml 中的 jar3):

public class Component implements AbstractComponent {

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

依赖jar3:

public class SpecialDepend {

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

主要在 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);
    }
}

输出是

This is component handle
This is tool

这很完美。

更新 2:

我尝试打印更多调试和一些不必要的代码,然后我发现,可以找到驱动程序 class 并实例化,但是 DriverManager.registerDriver 没有注册它。

所以问题变成了:为什么 DriverManager 不能从子 classloader 注册驱动程序加载?

更新3

contextClassLoader 是从 Thread.currentThread().getContextClassLoader() 获取的,但是通过 currentThread.setContextClassLoader(exclusiveClassLoader);

框架注入

为了仔细检查,我打印了哈希码,它是一样的。

然后我调试到 DriverManager,它已将驱动程序注册到内部列表中,但在那之后,getDrivers 将一无所获。

ClassLoader 首先在其父级中查找 classes,然后父级委托给其父级,依此类推。话虽如此,作为兄弟姐妹的 ClassLoader 无法看到彼此 classes.

方法 DriverManager#getDrivers() 也会在内部验证调用者 ClassLoader 是否可以使用 DriverManager#isDriverAllowed(Driver, ClassLoader) 加载 class。 这意味着即使你的 Driver 被添加到注册列表中,它也只是作为 DriverInfo 的一个实例添加,这意味着它只会按需加载(懒惰),并且仍然可能不会尝试加载时注册,这就是为什么你得到一个空列表。