使用 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
的一个实例添加,这意味着它只会按需加载(懒惰),并且仍然可能不会尝试加载时注册,这就是为什么你得到一个空列表。
我做了一些研究,但由于这种情况的复杂性,不适合我。
像 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
的一个实例添加,这意味着它只会按需加载(懒惰),并且仍然可能不会尝试加载时注册,这就是为什么你得到一个空列表。