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

4

6

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([email protected]/ClassLoader.java:1989)
    at java.lang.System.initPhase3([email protected]/System.java:2132)
Caused by: java.lang.ClassNotFoundException: org.project.common.CustomClassLoader
    at jdk.internal.loader.BuiltinClassLoader.loadClass([email protected]/BuiltinClassLoader.java:583)
    at jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass([email protected]/ClassLoaders.java:178)
    at java.lang.ClassLoader.loadClass([email protected]/ClassLoader.java:521)
    at java.lang.Class.forName0([email protected]/Native Method)
    at java.lang.Class.forName([email protected]/Class.java:415)
    at java.lang.ClassLoader.initSystemClassLoader([email protected]/ClassLoader.java:1975)
    at java.lang.System.initPhase3([email protected]/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?

Decrescendo answered 16/10, 2020 at 11:34 Comment(3)
What is the startup configuration you are using to start ? Have you tried the following format ? java -cp ./lib/* com.example.Main.Internuncial
What is the problem-statement? Are you trying to launch your application itself from (a) a custom-classloader or (b) are you trying to load specific application related classes after launching the application ?Internuncial
If you print System.out.println(Main.class.getClassLoader()); do you expect your class-loader to be specified or the standard system classloader to be printed (assuming com.example.Main is your main class) ?Internuncial
M
3

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.
  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.

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.

Maxinemaxiskirt answered 21/10, 2020 at 1:47 Comment(1)
Update: added link to Spring Boot executable JAR format description.Maxinemaxiskirt
B
2

Assuming you want to add custom jar to the classpath with Spring, do the following:

  1. 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>
    
  2. 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

Booker answered 26/10, 2020 at 9:16 Comment(0)
I
1

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).

Internuncial answered 27/10, 2020 at 1:42 Comment(0)
R
0

For Spring boot application use -Dloader.path to add different folder on classpath, like in example shown below :

java -cp app.jar -Dloader.path=/opt/lib/ org.springframework.boot.loader.PropertiesLauncher
Remuneration answered 14/4, 2023 at 12:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.