Skip to content

ClassLoader issues when used with Spring Boot on Java 19(+) #699

@zbocsor

Description

@zbocsor

Creating a Thread using its 5 parameter constructor with the inheritInheritableThreadLocals parameter set to false will result in using the system class loader since Java 19 (see this line in the JDK source) This means that the ThreadHelper.JDK9ThreadFactory will create threads that before Java 19 would inherit the parent thread's context class loader, however after Java 19, the created threads will use the system class loader.

This does not play nice with Spring Boot generated executable jars (see https://docs.spring.io/spring-boot/specification/executable-jar/index.html). This can cause some code running on "jaxws-engine-" threads to behave differently with Java 19(+).

Specifically on a project I'm currently working on, I noticed strange errors while trying to upgrade to Java 21. See this stack trace:

java.util.concurrent.ExecutionException: jakarta.xml.ws.WebServiceException: java.lang.Error: jakarta.xml.bind.JAXBException: Implementation of Jakarta XML Binding-API has not been found on module path or classpath.
 - with linked exception:
[java.lang.ClassNotFoundException: org.glassfish.jaxb.runtime.v2.ContextFactory]
	at com.sun.xml.ws.util.CompletedFuture.get(CompletedFuture.java:50)
	at [*********]
	at com.sun.xml.ws.client.AsyncResponseImpl.set(AsyncResponseImpl.java:104)
	at com.sun.xml.ws.client.sei.AsyncMethodHandler$SEIAsyncInvoker$1.onCompletion(AsyncMethodHandler.java:179)
	at com.sun.xml.ws.client.Stub$1.onCompletion(Stub.java:528)
	at com.sun.xml.ws.api.pipe.Fiber.completionCheck(Fiber.java:897)
	at com.sun.xml.ws.api.pipe.Fiber.run(Fiber.java:792)
	at com.sun.xml.ws.api.server.ThreadLocalContainerResolver$2$1.run(ThreadLocalContainerResolver.java:89)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1570)
Caused by: jakarta.xml.ws.WebServiceException: java.lang.Error: jakarta.xml.bind.JAXBException: Implementation of Jakarta XML Binding-API has not been found on module path or classpath.
 - with linked exception:
[java.lang.ClassNotFoundException: org.glassfish.jaxb.runtime.v2.ContextFactory]
	... 8 common frames omitted
Caused by: java.lang.Error: jakarta.xml.bind.JAXBException: Implementation of Jakarta XML Binding-API has not been found on module path or classpath.
 - with linked exception:
[java.lang.ClassNotFoundException: org.glassfish.jaxb.runtime.v2.ContextFactory]
	at com.sun.xml.ws.fault.ExceptionBean.<clinit>(ExceptionBean.java:173)
	at com.sun.xml.ws.fault.SOAPFaultBuilder.attachServerException(SOAPFaultBuilder.java:275)
	at com.sun.xml.ws.fault.SOAPFaultBuilder.createException(SOAPFaultBuilder.java:124)
	at com.sun.xml.ws.client.sei.StubHandler.readResponse(StubHandler.java:225)
	at com.sun.xml.ws.db.DatabindingImpl.deserializeResponse(DatabindingImpl.java:176)
	at com.sun.xml.ws.db.DatabindingImpl.deserializeResponse(DatabindingImpl.java:263)
	at com.sun.xml.ws.client.sei.AsyncMethodHandler$SEIAsyncInvoker$1.onCompletion(AsyncMethodHandler.java:148)
	... 7 common frames omitted
Caused by: jakarta.xml.bind.JAXBException: Implementation of Jakarta XML Binding-API has not been found on module path or classpath.
	at jakarta.xml.bind.ContextFinder.newInstance(ContextFinder.java:250)
	at jakarta.xml.bind.ContextFinder.newInstance(ContextFinder.java:238)
	at jakarta.xml.bind.ContextFinder.find(ContextFinder.java:386)
	at jakarta.xml.bind.JAXBContext.newInstance(JAXBContext.java:605)
	at jakarta.xml.bind.JAXBContext.newInstance(JAXBContext.java:546)
	at com.sun.xml.ws.fault.ExceptionBean.<clinit>(ExceptionBean.java:170)
	... 13 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.glassfish.jaxb.runtime.v2.ContextFactory
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
	at jakarta.xml.bind.ServiceLoaderUtil.nullSafeLoadClass(ServiceLoaderUtil.java:113)
	at jakarta.xml.bind.ServiceLoaderUtil.safeLoadClass(ServiceLoaderUtil.java:146)
	at jakarta.xml.bind.ContextFinder.newInstance(ContextFinder.java:248)
	... 18 common frames omitted

This seems to happen because the completion handler runs on the "jaxws-engine-" threads but outside of a Fiber's _doRun method. This causes the code to use the original context class loader of that thread, which in Java 19(+) is the system class loader. However as our project is run from a Spring Boot generated executable jar, this is incorrect, because it should use Spring Boot's class loader instead.

My proposal to fix this is by explicitely setting the context class loader of the threads created by ThreadHelper.JDK9ThreadFactory to the parent thread's class loader.
This seems to be the proper solution according to this JDK ticket: https://bugs.openjdk.org/browse/JDK-8290003?jql=labels%20%3D%20JEP-425.
Spring Boot documentation mentions this as well: https://docs.spring.io/spring-boot/specification/executable-jar/restrictions.html.

I created the following PR for this: #698.
@lukasj Kindly review and share your feedback. Thanks.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions