Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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?

like image 475
MrPlow Avatar asked Oct 16 '20 11:10

MrPlow


2 Answers

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.

like image 122
kriegaex Avatar answered Sep 30 '22 17:09

kriegaex


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

like image 23
Ravi raj Avatar answered Sep 30 '22 18:09

Ravi raj