Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

parallelStream() causing ClassNotFoundException with JAXB-API

In our application we sometimes get the following exception:

javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath.
 - with linked exception:
[java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory]

We already found out that this happens only if we use Collection.parallelStream() but not if we use Collection.stream().

We saw that JAXB uses Thread.currentThread().getContextClassLoader() for loading classes. We also saw, that when using parallelStream(), the threads for executing our command are using different class loaders. Sometimes it's a org.apache.catalina.loader.WebappClassLoader, and sometimes it's a jdk.internal.loader.ClassLoaders.AppClassLoader.

And it now seems, that the AppClassLoader does not know about the JAXB dependencies, whereas the WebappClassLoader does.

We are using Java 11 and the following Maven dependencies:

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>

Any idea what could be wrong? How can it be, that the AppClassLoader does not know about our dependencies?

like image 363
Ethan Leroy Avatar asked Apr 01 '19 10:04

Ethan Leroy


1 Answers

We had a different experience. The issue was that tomcat has a different classloader than each war (I think we're seeing something similar with spring boot apps; void-main was different than that of application-runner)..

Either way, the issue is that the "bootstrap" classloader doesn't have access to the jars in your application - and thus NO jaxb. So if you ever launch a thread (like ForkJoinPoolThread from StreamXX.parallel() or ForkJoinPool.commonThreadPool()) then THOSE threads will be from the bootstrap classloader, NOT your application's classloader.. So if the FIRST time your background task loads JAXB, they will run getClass().getContextClassLoader().getResourceAsStream("xxxx") and will not find the resource.

Our solution was to launch all background tasks in an explicit thread-pool and have an explicit thread-pool factory.. The thread-pool-factory captures the classloader from the invoking thread (the one initialized by the war or the spring-boot context). This classloader WILL have jaxb and friends.. So now each thread launched from this thread-pool factory has an explicit thr.setContextClassLoader(globalCL);....

Problem solved (via a hack)

like image 148
M. Maraist Avatar answered Nov 12 '22 01:11

M. Maraist