英文:
Spring boot runnable jar can't find classloader set via java.system.class.loader jvm parameter
问题
在这样的模块结构中:
项目
|
|- 共同模块
|- 应用模块
在这里,应用模块将共同模块作为依赖项,我在共同模块中定义了一个自定义类加载器类。应用模块具有-Djava.system.class.loader=org.project.common.CustomClassLoader
JVM参数,用于使用共同模块中定义的自定义类加载器。
在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)
为什么会发生这种情况?是因为在可运行的JAR中,类加载器类位于lib目录中的JAR中,因此类加载器试图在lib类被添加到类路径之前进行设置吗?除了将类加载器从共同模块移动到需要它的所有其他模块之外,我还能做些什么?
编辑:我尝试将自定义类加载器类从共同模块移到应用模块,但仍然收到相同的错误。这里发生了什么?
英文:
In a module structure like this:
project
|
|- common module
|- app module
Where app module has common module as a dependency, I have a custom classloader class defined in the common module. The app module has a -Djava.system.class.loader=org.project.common.CustomClassLoader
jvm parameter set to use that custom classloader defined in common module.
Running a spring boot project within IDEA this works perfectly. The custom classloader is found, set as a system classloader and everything works.
Compiling a runnable jar (using default spring-boot-maven-plugin without any custom properties), the jar itself has all the classes and within it's lib directory is the common jar which has the custom classloader. However running the jar with the -Djava.system.class.loader=org.project.common.CustomClassLoader
results in the following exception
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)
Why does this happen? 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? Is there anything I can do besides moving the classloader from common to all the other modules that need it?
EDIT: I've tried moving the custom classloader class from common module to app but I am still getting the same error. What is going on here?
答案1
得分: 2
> 在IDEA中运行Spring Boot项目,这个工作得很完美。自定义类加载器被找到,设置为系统类加载器,一切都运行正常。
因为IDEA将你的模块放在类路径上,其中一个模块包含了自定义类加载器。
> 是不是因为在可运行的jar文件中,类加载器类位于lib目录中的一个jar中,所以类加载器在将lib类添加到类路径之前就尝试设置了?
有点类似。lib目录中的类并没有被“添加到类路径”,但是可运行的Spring Boot应用程序自己的自定义类加载器知道从哪里找到这些类并且如何加载它们。
要更深入地理解java.system.class.loader
,请阅读ClassLoader.getSystemClassLoader()
的Javadoc(稍微重新格式化并添加了枚举):
> 1. 如果在首次调用此方法时定义了系统属性java.system.class.loader
,则该属性的值将被视为将作为系统类加载器返回的类的名称。
> 0. 该类使用默认的系统类加载器加载,必须定义一个公共构造函数,该构造函数接受一个类型为ClassLoader
的单参数,该参数用作委托父级。
> 0. 然后使用此构造函数创建一个实例,参数为默认的系统类加载器。
> 0. 所得到的类加载器被定义为系统类加载器。
> 0. 在构造过程中,类加载器应当特别注意避免调用getSystemClassLoader()
。如果检测到系统类加载器的循环初始化,则抛出IllegalStateException
。
这里的关键因素是第3点:用户定义的系统类加载器是由默认的系统类加载器加载的。当然,后者不知道如何从嵌套的JAR中加载内容。只有在JVM完全初始化之后,Spring Boot特殊的应用程序类加载器才会启动,然后可以读取那些嵌套的JAR。
也就是说,你在这里遇到了先有鸡还是先有蛋的问题:为了在JVM初始化期间找到你的自定义类加载器,你需要使用尚未初始化的Spring Boot可运行JAR类加载器。
如果你想知道上面的Javadoc在实践中是如何实现的,请查看OpenJDK的ClassLoader.initSystemClassLoader()
源代码。
> 除了将类加载器从公共位置移动到需要它的所有其他模块之外,我还能做些什么吗?
即使这样做,如果你坚持使用可运行的JAR,也没有用。你可以考虑以下做法:
- 将应用程序打包成可运行的JAR之前,不要压缩它,以普通的Java应用程序方式运行,所有应用程序模块(特别是包含自定义类加载器的模块)都在类路径上。
- 将自定义类加载器提取到可运行的JAR之外的一个单独模块,并在运行可运行的JAR时将其放在类路径上。
- 通过
Thread.setContextClassLoader()
等方式设置自定义类加载器,而不是尝试将其用作系统类加载器,如果这是可行的选项。
更新于2020-10-28: 在文档“可执行JAR格式”中,我在“可执行JAR限制”下找到了以下内容:
> 系统类加载器:启动的应用程序在加载类时应使用Thread.getContextClassLoader()
(大多数库和框架默认如此)。尝试使用ClassLoader.getSystemClassLoader()
加载嵌套的JAR类会失败。java.util.Logging
始终使用系统类加载器。因此,你应该考虑使用不同的日志记录实现。
这证实了我上面的写的内容,特别是关于使用线程上下文类加载器的最后一个要点。
英文:
> Running a spring boot project within IDEA this works perfectly. The custom classloader is found, set as a system classloader and everything works.
Because IDEA puts your modules on the class path and one of them contains the custom class loader.
> 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?
Kind of. The lib classes are not "added to the class path", but the runnable Spring Boot app's own custom class loader knows where to find and how to load them.
For a deeper understanding of java.system.class.loader
, please read the Javadoc for ClassLoader.getSystemClassLoader()
(slightly reformatted with added enumeration):
> 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.
> 0. 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.
> 0. An instance is then created using this constructor with the default system class loader as the parameter.
> 0. The resulting class loader is defined to be the system class loader.
> 0. 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.
The decisive factor here is #3: The user-defined system class loader is loaded by the default system class loader. The latter of course has no clue about how to load something from a nested JAR. Only later, after the JVM is fully initialised and Spring Boot's special application class loader kicks in, can those nested JARs be read.
I.e. you are having a chicken vs. egg problem here: In order to find your custom class loader during JVM initialisation, you would need to use the Spring Boot runnable JAR class loader which has not been initialised yet.
If you want to know how what the Javadoc above describes is done in practice, take a look at the 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?
Even that would not help if you insist in using the runnable JAR. What you could do is either of these:
- Run your application without zipping it up into a runnable JAR, but as a normal Java application with all application modules (especially the one containing the custom class loader) on the class path.
- Extract your custom class loader into a separate module outside of the runnable JAR and put it on the class path when running the runnable JAR.
- Set your custom class loader via
Thread.setContextClassLoader()
or so instead of trying to use it as a system class loader, if that would be a viable option.
Update 2020-10-28: In the document "The Executable Jar Format" I found this under "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.
This confirms what I wrote above, especially my last bullet point about using the thread context class loader.
答案2
得分: 2
假设您想要在Spring中将自定义JAR文件添加到类路径中,请按照以下步骤操作:
- 使用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>
- 在命令行运行应用程序时,请使用以下命令:
java -cp target/demo-0.0.1-SNAPSHOT.jar -Dloader.path=<自定义JAR文件的路径> org.springframework.boot.loader.PropertiesLauncher
这将启动您的应用程序并加载自定义类加载器。
简而言之,关键是要使用**-Dloader.path和org.springframework.boot.loader.PropertiesLauncher**。
英文:
Assuming you want to add custom jar to the classpath with Spring, do the following:
-
Generate the jar file with the maven jar plugin
<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>
-
While running the application from the command line, use the below command
java -cp target/demo-0.0.1-SNAPSHOT.jar -Dloader.path=<Path to the Custom Jar file>
org.springframework.boot.loader.PropertiesLauncher
This should launch your app while loading the Custom Classloader as well
In short, the trick is, to use the -Dloader.path along with org.springframework.boot.loader.PropertiesLauncher
答案3
得分: 1
一个类似以下命令行启动的应用程序 -
java -cp ./lib/* com.example.Main
理论上应该是足够的。
需要一些关于应用程序使用方式的明确信息。
主类是否尝试从自定义类加载器启动(假设可以这样做),还是在启动后需要使用自定义类加载器加载特定的与应用程序相关的类(以及相关特权)?
已在上面的注释中提出了这些问题(计划在明确之后在此处更新答案)。
附注:尚未真正考虑使用“模块”,但认为上述语法仍适用于更新的 JDK(JDK 8之后)。
英文:
An application launched on the lines of -
java -cp ./lib/* com.example.Main
would ideally be sufficient.
Will need some clarity on how the application is being used.
Is the main class itself being attempted to be launched from a custom class loader (assuming its possible to do so) or whether post launch specific application related classes are required to be loaded with a custom class-loader (and associated privileges)?
Have asked those questions in the comments above (planning to update the answers here once have more clarity).
PS: Haven't really factored the use of 'modules' yet but believe the above syntax would still hold for the newer jdk's (after jdk 8).
答案4
得分: 0
对于Spring Boot应用程序,请使用-Dloader.path来添加不同的文件夹到类路径中,就像下面的示例一样:
java -cp app.jar -Dloader.path=/opt/lib/ org.springframework.boot.loader.PropertiesLauncher
英文:
For Spring boot application use <b>-Dloader.path</b> to add difrent folder on classpath, like in example below :
java -cp app.jar -Dloader.path=/opt/lib/ org.springframework.boot.loader.PropertiesLauncher
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论