I would like to use jdk.internal.loader.ClassLoaders$AppClassLoader
instead of spring-boots org.springframework.boot.loader.LaunchedURLClassLoader
. However, I am uncertain of the implications of how this will affect the spring-boot runtime.
We recently ran into a java.lang.InstantiationError
when making Spring repository calls inside java's parallelStream()
. I learned that threads in the parallel stream are using the AppClassLoader
and thus are unable to find the application classes residing in LaunchedURLClassLoader
, a child of AppClassLoader
in the classloader hierarchy.
The spring-boot documentation shows you can run an unpacked spring-boot jar without using their classloader. This conveniently means that application classes are also loaded into AppClassLoader
, resolving our issue with the parallelStream()
.
$ java -cp BOOT-INF/classes:BOOT-INF/lib/* com.example.MyApplication
NB: This is also how Intellij IDEA runs spring-boot applications.
Who knows if the spring-boot classloader is doing more than enabling executable jar and war files that I should know about. I found the documentation not explicit enough. Is it safe to not use spring-boot classloader in production?
I also had intermittent issues with AppClassLoader being suddenly used instead of LaunchedURLClassLoader (even outside of parallel execution), leading to resources not being found.
The latest version of the documentation has a Efficient deployments section that says it is faster to run the unpacked archive.
Certain PaaS implementations may also choose to unpack archives before they run. For example, Cloud Foundry operates this way.
Knowing that, I could say it's absolutely safe to do it in production. I'd even say that it's safer, as it will be simpler at runtime, but it's my humble opinion.
Once you have unpacked the jar file, you can also get an extra boost to startup time by running the app with its "natural" main method [...]:
$ jar -xf myapp.jar
$ java -cp "BOOT-INF/classes:BOOT-INF/lib/*" com.example.MyApplication
On Windows, replace :
by ;
in casspath:
> java -cp "BOOT-INF/classes;BOOT-INF/lib/*" com.example.MyApplication
As you said, it conveniently means that we get rid of Spring Boot's custom LaunchedURLClassLoader
as everything is loaded by AppClassLoader
!
You can now use ForkJoinPool (parallel()
, etc.) without ClassLoader issues:
[main] Test : CCL: jdk.internal.loader.ClassLoaders$AppClassLoader@2a139a55
[ForkJoinPool.commonPool-worker-1] Test : CompletableFuture.runAsync CCL: jdk.internal.loader.ClassLoaders$AppClassLoader@2a139a55
Warning - there's only one note:
Using the JarLauncher over the application’s main method has the added benefit of a predictable classpath order. The jar contains a classpath.idx file which is used by the JarLauncher when constructing the classpath.
It means that running the exploded archive with JarLauncher instead of LaunchedURLClassLoader is safe and consistent because you keep the same classpath order. But JarLauncher launches the LaunchedURLClassLoader, so it does not solve our issue.
So beware of unpredictable classpath order when using the "natural" main method. It seems to be the only disadvantage.
Personally, I did not have any issue with classpath ordering.
Read:
Going further:
Looking at LaunchedURLClassLoader, which extends URLClassLoader, and analyzing tests in LaunchedURLClassLoaderTests I can say that it simply adds logic that deals with JAR resource loading and nested JARs. There have been performance issues like #4582 that have been fixed in the past by adding the "fast exception" mechanism.