Spring boot runnable jar 找不到通过 java.system.class.loader jvm 参数设置的类加载器

MrP*_*low 5 java spring classloader spring-boot

在这样的模块结构中:

项目
|
|- 通用模块
|- 应用模块

在 app 模块具有公共模块作为依赖项的情况下,我在公共模块中定义了一个自定义类加载器类。app 模块有一个-Djava.system.class.loader=org.project.common.CustomClassLoaderjvm 参数集来使用公共模块中定义的自定义类加载器。

在 IDEA 中运行一个 spring boot 项目,这非常有效。找到自定义类加载器,设置为系统类加载器,一切正常。

编译一个可运行的 jar(使用没有任何自定义属性的默认 spring-boot-maven-plugin),jar 本身包含所有类,并且在它的 lib 目录中是具有自定义类加载器的公共 jar。但是运行 jar 的-Djava.system.class.loader=org.project.common.CustomClassLoader结果是以下异常

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)

Run Code Online (Sandbox Code Playgroud)

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

编辑:我尝试将自定义类加载器类从通用模块移动到应用程序,但我仍然遇到相同的错误。这里发生了什么?

kri*_*aex 3

在 IDEA 中运行 Spring Boot 项目效果非常好。找到自定义类加载器,将其设置为系统类加载器,一切正常。

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

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

有点儿。lib 类不会“添加到类路径中”,但可运行的 Spring Boot 应用程序自己的自定义类加载器知道在哪里查找以及如何加载它们。

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

  1. 如果首次调用此方法时定义了系统属性java.system.class.loader,则该属性的值将被视为将作为系统类加载器返回的类的名称。
  2. 该类是使用默认系统类加载器加载的,并且必须定义一个公共构造函数,该构造函数ClassLoader采用用作委托父级的单个类型参数。
  3. 然后使用此构造函数以默认系统类加载器作为参数创建一个实例。
  4. 生成的类加载器被定义为系统类加载器。
  5. 在构造过程中,类加载器应该非常小心,避免调用getSystemClassLoader(). 如果检测到系统类加载器的循环初始化,则会IllegalStateException抛出异常。

这里的决定性因素是#3:用户定义的系统类加载器由默认系统类加载器加载。后者当然不知道如何从嵌套 JAR 中加载某些内容。只有稍后,在 JVM 完全初始化并且 Spring Boot 的特殊应用程序类加载器启动后,才能读取这些嵌套的 JAR。

也就是说,您在这里遇到了先有鸡还是先有蛋的问题:为了在 JVM 初始化期间找到自定义类加载器,您需要使用尚未初始化的 Spring Boot 可运行 JAR 类加载器。

如果您想了解上面描述的 Javadoc 在实践中是如何完成的,请查看ClassLoader.initSystemClassLoader().

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

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

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

2020-10-28 更新:在文档“可执行 Jar 格式”中,我在“可执行 Jar 限制”下找到了以下内容:

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

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