【问题标题】:Load SPI class with URLClassLoader rise ClassNotFoundException用 URLClassLoader 加载 SPI 类上升 ClassNotFoundException
【发布时间】:2021-09-23 14:57:53
【问题描述】:

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

Child first class loader and Service Provider Interface (SPI)

像 flink 或 tomcat 一样,我的应用程序作为带有平台和系统类加载器的框架运行。 框架加载插件作为模块和插件可能依赖于一些库,因此请定义:

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

框架会像这样创建两个类加载器:

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

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

但是使用 SPI 的 JDBC 驱动程序com.mysql.cj.jdbc.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);

这工作正常,但之后:

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 中运行,但我的类加载器父级中的驱动程序加载不会在子级中搜索,所以我将 currentThreadClassLoader 设置为父级但仍然出现异常。

更新 1:

URI-TO-depend-jars 是 File.toURI().toURL() 的数组。 这个设计与demo配合得很好,所以我认为它应该是正确的。

通过调试,ClassLoader 父链为
ModuleLoader -&gt; DependLoader
而系统类加载器是
ModuleLoader -&gt; DependLoader -&gt; BuiltinAppClassLoader -&gt; PlatformClassLoader -&gt; 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:

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

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

更新3

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

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

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

【问题讨论】:

  • {URI-TO-PLUGIN-JAR} 的实际值是多少?这可能会有所帮助stackoverflow.com/a/738422/15273968
  • 在更新 2 上,您能看到哪个类加载器正在加载 DriverManager?而这个contextClassLoader 引用是当前线程中java.lang.Thread#getContextClassLoader() 的产物吗?
  • @SaleemKhair 我用updated3更新,我发现了一些奇怪的东西,据我所知,从另一个类加载器加载的类即使关闭了URLClassLoader,仍然可以工作。
  • 一个类加载器首先在它的父类中寻找类,然后父类委托给它的父类,依此类推,所以作为兄弟的类加载器不能看到彼此的类,DriverManager#getDrivers() 也在内部验证是否调用者类加载器可以使用DriverManager#isDriverAllowed(Driver, ClassLoader) 加载类,因此即使在注册中填充了驱动程序,它仍然可能没有注册,这就是你得到一个空列表的原因。
  • 解决了我的问题,我调试到 DriverManager,getDriver 将通过 getCaller 和调用者的 Classloader 进行检查。我需要对类加载器继承树做一些工作,但这会偏离主题,感谢这个提示!考虑写一个答案? @SaleemKhair

标签: java classloader urlclassloader


【解决方案1】:

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

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

【讨论】:

    猜你喜欢
    • 2014-09-01
    • 2015-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多