Spring Boot Runnable JAR无法找到通过java.system.class.loader JVM参数设置的类加载器

2022-04-12 00:00:00 spring java spring-boot classloader

在如下的模块结构中:

项目

|-公共模块
|-app模块

在app模块将公共模块作为依赖项的情况下,我有一个在公共模块中定义的定制类加载器类。应用程序模块-Djava.system.class.loader=org.project.common.CustomClassLoaderJVM参数设置为使用公共模块中定义自定义类加载器。

在IDEA中运行一个Spring Boot项目,这可以完美地工作。找到自定义类加载器,将其设置为系统类加载器,一切正常。

编译一个可运行的JAR(使用没有任何定制属性的默认Spring-Boot-maven-plugin),JAR本身拥有所有类,并且在它的lib目录中是具有定制类加载器的公共JAR。但是,使用-Djava.system.class.loader=org.project.common.CustomClassLoader运行JAR会导致以下异常

java.lang.Error: org.project.common.CustomClassLoader
    at java.lang.ClassLoader.initSystemClassLoader(java.base@12.0.2/ClassLoader.java:1989)
    at java.lang.System.initPhase3(java.base@12.0.2/System.java:2132)
Caused by: java.lang.ClassNotFoundException: org.project.common.CustomClassLoader
    at jdk.internal.loader.BuiltinClassLoader.loadClass(java.base@12.0.2/BuiltinClassLoader.java:583)
    at jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(java.base@12.0.2/ClassLoaders.java:178)
    at java.lang.ClassLoader.loadClass(java.base@12.0.2/ClassLoader.java:521)
    at java.lang.Class.forName0(java.base@12.0.2/Native Method)
    at java.lang.Class.forName(java.base@12.0.2/Class.java:415)
    at java.lang.ClassLoader.initSystemClassLoader(java.base@12.0.2/ClassLoader.java:1975)
    at java.lang.System.initPhase3(java.base@12.0.2/System.java:2132)

为什么会发生这种情况?是否因为在Runnable JAR中,类加载器类位于lib目录中的jar中,所以类加载器试图在将lib类添加到类路径之前进行设置?除了将类加载器从公共模块移动到所有其他需要它的模块之外,我还能做什么吗?

编辑:我尝试将自定义类加载器类从公共模块移动到应用程序,但仍然收到相同的错误。这是怎么回事?


解决方案

在IDEA中运行一个Spring Boot项目,这可以完美地工作。找到自定义类加载器,将其设置为系统类加载器,一切正常。

因为IDEA将模块放在类路径上,并且其中一个模块包含自定义类加载器。

是否因为在可运行的JAR中,类加载器类位于lib目录中的jar中,因此在将lib类添加到类路径之前尝试设置类加载器?

差不多吧。库类没有添加到类路径中,但可运行的Spring Boot应用程序自己的自定义类加载器知道在哪里找到它们以及如何加载它们。

要更深入地了解java.system.class.loader,请阅读ClassLoader.getSystemClassLoader()的Javadoc(添加了枚举后略微重新格式化):

  1. 如果第一次调用此方法时定义了系统属性java.system.class.loader,则该属性的值将被视为将作为系统类加载器返回的类的名称。
  2. 该类是使用默认系统类加载器加载的,并且必须定义一个公共构造函数,该构造函数接受用作委托父级的类型ClassLoader的单个参数。
  3. 然后使用此构造函数以默认系统类加载器作为参数创建实例。
  4. 结果类加载器被定义为系统类加载器。
  5. 在构造过程中,类加载器要特别注意避免调用getSystemClassLoader()。如果检测到系统类加载器的循环初始化,则引发IllegalStateException
这里的决定性因素是#3:用户定义的系统类加载器由默认的系统类加载器加载。当然,后者不知道如何从嵌套JAR中加载内容。只有在JVM完全初始化并启动了Spring Boot的特殊应用程序类加载器之后,才能读取这些嵌套的JAR。

即您遇到了鸡和蛋的问题:为了在JVM初始化期间找到您的自定义类加载器,您需要使用尚未初始化的Spring Boot Runnable JAR类加载器。

如果您想知道上面所描述的Javadoc在实践中是如何实现的,请查看OpenJDKsource code of ClassLoader.initSystemClassLoader()

除了将类加载器从公共模块移动到所有其他需要它的模块之外,我还能做什么吗?

如果您坚持使用Runnable JAR,即使这样也无济于事。您可以执行以下任一操作:

  • 运行您的应用程序,而不是将其压缩到可运行的JAR中,而是将其作为一个普通的Java应用程序运行,所有应用程序模块(尤其是包含自定义类加载器的模块)都位于类路径上。
  • 将您的自定义类加载器提取到可运行JAR外部的单独模块中,并在运行可运行JAR时将其放在类路径上。
  • 通过Thread.setContextClassLoader()左右设置您的自定义类加载器,而不是尝试将其用作系统类加载器(如果这是一个可行的选项)。

更新2020-10-28:在可执行Jar格式文档中,我在"Executable Jar Restrictions"下找到了:

系统类加载器:启动的应用程序在加载类时应该使用Thread.getContextClassLoader()(大多数库和框架默认这样做)。尝试使用ClassLoader.getSystemClassLoader()加载嵌套的JAR类失败。java.util.Logging始终使用系统类加载器。因此,您应该考虑不同的日志记录实现。

这证实了我在上面所写的内容,特别是我关于使用线程上下文类加载器的最后一个要点。

相关文章