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

Spring boot runnable jar can't find classloader set via java.system.class.loader jvm parameter

在这样的模块结构中:

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

在应用程序模块具有公共模块作为依赖项的地方,我在公共模块中定义了一个自定义 classloader class。应用程序模块有一个 -Djava.system.class.loader=org.project.common.CustomClassLoader jvm 参数集,以使用在通用模块中定义的自定义 classloader。

运行 IDEA 中的一个 spring 引导项目,这非常有效。找到自定义 classloader,设置为系统 classloader,一切正常。

编译一个可运行的 jar(使用默认 spring-boot-maven-plugin 没有任何自定义属性),jar 本身具有所有 classes 并且在它的 lib 目录中是通用 jar它有自定义 classloader。但是 运行 带有 -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)

为什么会这样?是不是因为在可运行的 jar 中,classloader class 位于 lib 目录中的 jar 中,所以 classloader 试图在添加 lib classes 之前设置class路径?除了将 classloader 从 common 移动到所有其他需要它的模块之外,我还能做些什么吗?

EDIT:我已经尝试将自定义 classloader class 从通用模块移动到应用程序,但我仍然遇到相同的错误。这是怎么回事?

Running a spring boot project within IDEA this works perfectly. The custom classloader is found, set as a system classloader and everything works.

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

Is it because in the runnable jar the classloader class is in a jar in lib directory so the classloader is trying to get set before the lib classes were added to the classpath?

有点。 lib classes 没有“添加到 class 路径”,但可运行 Spring 启动应用程序自己的自定义 class 加载器知道在哪里找到它们以及如何加载它们。

为了更深入地了解 java.system.class.loader,请阅读 ClassLoader.getSystemClassLoader() 的 Java 文档(通过添加枚举稍微重新格式化):

  1. If the system property java.system.class.loader is defined when this method is first invoked then the value of that property is taken to be the name of a class that will be returned as the system class loader.
  2. The class is loaded using the default system class loader and must define a public constructor that takes a single parameter of type ClassLoader which is used as the delegation parent.
  3. An instance is then created using this constructor with the default system class loader as the parameter.
  4. The resulting class loader is defined to be the system class loader.
  5. During construction, the class loader should take great care to avoid calling getSystemClassLoader(). If circular initialization of the system class loader is detected then an IllegalStateException is thrown.

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

即您在这里遇到先有鸡还是先有蛋的问题:为了在 JVM 初始化期间找到您的自定义 class 加载程序,您需要使用 Spring Boot runnable JAR class 加载程序,它没有尚未初始化。

如果您想知道上面 Javadoc 描述的内容在实践中是如何完成的,请查看 OpenJDK source code of ClassLoader.initSystemClassLoader().

Is there anything I can do besides moving the classloader from common to all the other modules that need it?

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

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

2020-10-28 更新: 在文档“The Executable Jar Format”中,我在 "Executable Jar Restrictions":

下找到了这个

System classLoader: Launched applications should use Thread.getContextClassLoader() when loading classes (most libraries and frameworks do so by default). Trying to load nested jar classes with ClassLoader.getSystemClassLoader() fails. java.util.Logging always uses the system classloader. For this reason, you should consider a different logging implementation.

这证实了我上面写的内容,尤其是我关于使用线程上下文 class 加载程序的最后一个要点。

假设您想使用 Spring 将自定义 jar 添加到类路径中,请执行以下操作:

  1. 使用maven jar插件生成jar文件

     <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-jar-plugin</artifactId>
         <configuration>
             <archive>
                 <manifest>
                     <addClasspath>true</addClasspath>
                     <classpathPrefix>libs/</classpathPrefix>
                     <mainClass>
                         com.demo.DemoApplication
                     </mainClass>
                 </manifest>
             </archive>
         </configuration>
     </plugin>
    
  2. 当运行 从命令行运行应用程序时,使用下面的命令

java -cp target/demo-0.0.1-SNAPSHOT.jar -Dloader.path=<Path to the Custom Jar file> org.springframework.boot.loader.PropertiesLauncher

这应该会在加载自定义类加载器的同时启动您的应用程序

简而言之,诀窍是使用 -Dloader.pathorg.springframework.boot.loader.PropertiesLauncher

在 -

行启动的应用程序
java -cp ./lib/* com.example.Main

理想情况下就足够了。

需要清楚地了解应用程序的使用方式。 主 class 本身是否正在尝试从自定义 class 加载程序启动(假设可以这样做)或者是否需要 post 启动与特定应用程序相关的 classes加载自定义 class-loader(和相关权限)?

已经在上面的评论中提出了这些问题(计划在此处更新答案以便更清楚)。

PS:还没有真正考虑 'modules' 的使用,但相信上述语法仍然适用于较新的 jdk(在 jdk 之后) 8).