32

TLDR: On Java 9/10, a web app in Tomcat has no access to JAXB even though its reference implementation is present on the class path.

Edit: No, this is not a duplicate of How to resolve java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException in Java 9 - as you can tell by the What I tried section, I already tried the proposed solutions.

The Situation

We have a web app that runs on Tomcat and depends on JAXB. During our migration to Java 9 we opted for adding the JAXB reference implementation as a regular dependency.

Everything worked when launching the app from the IDE with embedded Tomcat, but when running it on a real Tomcat instance, I get this error:

Caused by: java.lang.RuntimeException: 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]
    at [... our-code ...]
Caused by: javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath.
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:278) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.ContextFinder.find(ContextFinder.java:421) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:721) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:662) ~[jaxb-api-2.3.0.jar:2.3.0]
    at [... our-code ...]
Caused by: java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory
    at jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582) ~[?:?]
    at jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:190) ~[?:?]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:499) ~[?:?]
    at javax.xml.bind.ServiceLoaderUtil.nullSafeLoadClass(ServiceLoaderUtil.java:122) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.ServiceLoaderUtil.safeLoadClass(ServiceLoaderUtil.java:155) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:276) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.ContextFinder.find(ContextFinder.java:421) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:721) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:662) ~[jaxb-api-2.3.0.jar:2.3.0]
    at [... our-code ...]

Note:

Implementation of JAXB-API has not been found on module path or classpath.

These are the relevant files in webapps/$app/WEB-INF/lib:

jaxb-api-2.3.0.jar
jaxb-core-2.3.0.jar
jaxb-impl-2.3.0.jar

What is going on here?

What I tried

Adding JARs to Tomca's CLASSPATH

Maybe it helps to add the JARs to Tomcat's class path in setenv.sh?

CLASSPATH=
    .../webapps/$app/WEB-INF/lib/jaxb-api-2.3.0.jar:
    .../webapps/$app/WEB-INF/lib/jaxb-impl-2.3.0.jar:
    .../webapps/$app/WEB-INF/lib/jaxb-core-2.3.0.jar:
    .../webapps/$app/WEB-INF/lib/javax.activation-1.2.0.jar

Nope:

Caused by: javax.xml.bind.JAXBException: ClassCastException: attempting to cast
jar:file:.../webapps/$app/WEB-INF/lib/jaxb-api-2.3.0.jar!/javax/xml/bind/JAXBContext.class to
jar:file:.../webapps/$app/WEB-INF/lib/jaxb-api-2.3.0.jar!/javax/xml/bind/JAXBContext.class.
Please make sure that you are specifying the proper ClassLoader.    
    at javax.xml.bind.ContextFinder.handleClassCastException(ContextFinder.java:157) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:300) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:286) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.ContextFinder.find(ContextFinder.java:409) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:721) ~[jaxb-api-2.3.0.jar:2.3.0]
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:662) ~[jaxb-api-2.3.0.jar:2.3.0]
    at de.disy.gis.webmapserver.factory.DefaultWmsRequestFactory.initializeCommandExtractor(DefaultWmsRequestFactory.java:103) ~[cadenza-gis-webmapserver-7.7-SNAPSHOT.jar:7.6]
    at de.disy.gis.webmapserver.factory.DefaultWmsRequestFactory.lambda$new$0(DefaultWmsRequestFactory.java:87) ~[cadenza-gis-webmapserver-7.7-SNAPSHOT.jar:7.6]

That's clearly the same class, so apparently it has been loaded by two class loaders. I suspect the system class loader and the app's class loader, but why would loading JAXBContext be delegated to the system class loader once but not always? It almost looks as if the delegation behavior of the app's class loader changes while the program runs.

Adding the module

I don't really want to add java.xml.bind, but I tried it anyways by adding this to catalina.sh:

JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-modules=java.xml.bind"

Doesn't work either, though:

Caused by: java.lang.ClassCastException:
java.xml.bind/com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl
cannot be cast to com.sun.xml.bind.v2.runtime.JAXBContextImpl
    at [... our-code ...]

Apart from the different class and stack trace, this is in line with what happened earlier: The class JAXBContextImpl was loaded twice, once from java.xml.bind (must have been the system class loader) and one other time (I assume by the app's loader from the JAR).

Searching for bugs

Searching Tomcat's bug database I found #62559. Could that be the same error?

Adding JAR's to Tomcat's lib

Following advice given on the Tomcat user mailing list, I added the JAXB JARs to Tomcat's CATALINA_BASE/lib directory, but got the same error as in the application's lib folder.

Nicolai Parlog
  • 47,972
  • 24
  • 125
  • 255

5 Answers5

18

Analysis

First some random facts:

  • if not given a class loader, JAXBContext::newInstance will use the thread's context class loader when looking for the JAXB implementation - this is the case even if you call newInstance(Class...) (one might mistakenly think it uses the provided class instances' loader)
  • Tomcat builds a small class loader hierarchy to separate web applications from one another
  • by not relying on the module java.xml.bind, in Java 9, JAXB classes are not loaded by the bootstrap or system class loader

So here's what happened on Java 8:

  • we don't pass a class loader to JAXB (oops), so it uses the thread's context class loader
  • our conjecture is that Tomcat does not explicitly set the context class loader and so it will end up being the same one that loaded Tomcat: the system class loader
  • that's dandy because the system class loader sees the entire JDK and hence the JAXB implementation included therein

Java 9 enters - the piano stops playing and everybody puts down their scotch:

  • we added JAXB as a regular dependency and so it is loaded by the web app's class loader
  • just as on Java 8, JAXB searches the system class loader, though, and that one can't see the app's loader (only the other way around)
  • JAXB fails to find the implementation and goes belly up

Solution

The solution is to make sure JAXB uses the right class loader. We know of three ways:

  • call Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); but that's not really a good idea
  • create a context resolver, but that requires JAX-WS and that feels like replacing one evil with another
  • use the package-accepting variant of JAXBContext::newInstance (Javadoc from Java EE 7) that also takes a class loader and pass the correct loader, although that requires some refactoring

We used the third option and refactored towards the package-accepting variant of JAXBContext::newInstance. Menial work, but fixed the problem.

Note

User curlals provided the critical piece of information, but deleted their answer. I hope it was not because I asked for a few edits. All credit/karma should go to them! @curlals: If you restore and edit your answer, I will accept and upvote it.

izogfif
  • 6,000
  • 2
  • 35
  • 25
Nicolai Parlog
  • 47,972
  • 24
  • 125
  • 255
  • random facts here, while interesting, are a great example of "over theoretical" because taken together you are making a statement that is not correct. actually, what is happening has nothing to do with the jvm. common sense, tells me, that because works with tomcat 9 as reg java app, and does not with tomcat 9 as service, with the same jvm (10), that the issue is a tomcat issue. Solution bellow worked!!! – tom Nov 29 '18 at 21:33
  • @tom, that my answer does not apply to your specific situation does not make it incorrect. It just means that our situations were different. You seem to ignore that the solution that worked for you didn't work for me, whereas this one _did_ solve my problem. – Nicolai Parlog Nov 30 '18 at 07:19
  • Ah... ...so where is your solution that you said was deleted by curlals? Can you please add it to your accepted solution so that it is clear that you resolved/how you finally chose to resolve/ your issue? – tom Nov 30 '18 at 10:10
  • _curlals_ did not provide a solution as much as helping me understand what the problem really was. I added a little but of structure and a sentence explaining which of the three possible solutions we picked. – Nicolai Parlog Dec 03 '18 at 21:47
  • @Nicolai , could you please provide details regarding your 3rd option solution? I run into same problem (while using parallelStream()) and the 1.option works, but I would better avoid setting classloader to threads if there is other option. My example project could be find here: https://github.com/troger19/xml_classloader – troger19 Jan 11 '19 at 22:55
  • @troger19: I added a link to the Javadoc of the `newInstance` overload we ended up using. If you need more details, you have to let me know what exactly they need to explain. – Nicolai Parlog Jan 16 '19 at 04:59
  • @NicolaiParlog why do you think setting TCCL is a bad idea? –  Jan 28 '21 at 10:24
  • @KirillBazarov Unfortunately, I don't remember whether we had specific reasons to come to that conclusion or whether it was just general caution - as you can see, class loader trickery gets complicated really quickly, so I prefer not to touch it if possible. – Nicolai Parlog Feb 03 '21 at 15:15
9

Try the following and its dependencies. See a Maven repository for latest version.

<dependency>
  <groupId>org.glassfish.jaxb</groupId>
  <artifactId>jaxb-runtime</artifactId>
  <version>2.3.0.1</version>
</dependency>

It also contains the Java Service Loader descriptors. See Using JAXB in Java 9+

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
Philippe Marschall
  • 4,452
  • 1
  • 34
  • 52
  • No, unfortunately it's the exact same error message as with _com.sun.xml.bind : jaxb-impl : 2.3.0_. – Nicolai Parlog Jul 26 '18 at 10:23
  • Ja baby! Thank you!!! ...fyi, this with jdk 10, tomcat 9, spring 5.3, hibernate 5.3 etc... ...and only had this CNF error when tomcat was running as service. with tomcat as regular java app from cmd line, worked fine.... why? – tom Nov 29 '18 at 21:29
  • can only edit a comment for 5 minutes? wow. SO can do better )). at least i can ))))). ....spring 5.3 -> spring 5 – tom Nov 29 '18 at 21:35
5

I had this issue using Spring Boot (version 2.2.6) with embedded Tomcat in a specific part of my code where I used a CompletableFuture. The code worked perfectly with Java 8 and related unit test passed in Java 12. The issue appeared only when the application was executed inside Tomcat using Java 11 or 12.

Debugging the problem I discovered the issue was related to the fact that a different ClassLoader is used inside the CompletableFuture's Runner.

// here Thread.currentThread().getContextClassLoader().getClass()
// returns org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader
return CompletableFuture.runAsync(() -> {
    // here returns jdk.internal.loader.ClassLoaders$AppClassLoader
});

The second ClassLoader is not able to load the JAXB classes. This behavior seems to be present only with Java 9+, indeed before Java 9 ForkJoinPool.common() returned an Executor with a ClassLoader of your main Thread, but after Java 9 it returns an executor with system ClassLoader.

Since the CompletableFuture.runAsync() method accepts an Executor as second parameter, it is possible to set the desired Executor in the code. Here an example of a possible solution.

First, define a proper ForkJoinWorkerThreadFactory:

public class JaxbForkJoinWorkerThreadFactory implements ForkJoinWorkerThreadFactory {

    private final ClassLoader classLoader;

    public JaxbForkJoinWorkerThreadFactory() {
        classLoader = Thread.currentThread().getContextClassLoader();
    }

    @Override
    public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
        ForkJoinWorkerThread thread = new JaxbForkJoinWorkerThread(pool);
        thread.setContextClassLoader(classLoader);
        return thread;
    }

    private static class JaxbForkJoinWorkerThread extends ForkJoinWorkerThread {

        private JaxbForkJoinWorkerThread(ForkJoinPool pool) {
            super(pool);
        }
    }
}

Then pass an Executor using that factory to the runAsync() method:

return CompletableFuture.runAsync(() -> {
    // now you have the right ClassLoader here
}, getJaxbExecutor());

private ForkJoinPool getJaxbExecutor() {
    JaxbForkJoinWorkerThreadFactory threadFactory = new JaxbForkJoinWorkerThreadFactory();
    int parallelism = Math.min(0x7fff /* copied from ForkJoinPool.java */, Runtime.getRuntime().availableProcessors());
    return new ForkJoinPool(parallelism, threadFactory, null, false);
}
xonya
  • 2,146
  • 29
  • 37
1

TL;DR

A simple solution that worked for me is just to upgrade the Hibernate version.

I used Hibernate with version of 5.2.10.Final and they rely on JAXB. However, when I replaced undertow with Tomcat, that dependency went missing. I found this issue but none of the answers really solved my issue. When I found that jpa-model-gen was the issue I quickly realized, that it is the onlt Hibernate dependecy only that is looking for JAXB. Updating the hibernate version to a higher one solved my problem.

Alan Sereb
  • 2,358
  • 2
  • 17
  • 31
0

I too have experienced with the similar issue while using JAXB, i.e

Implementation of JAXB-API has not been found

which occurs randomly and it was harder to reproduce. Fortunately I had found a system environment where the above error is continuous, while other environments it worked smoothly.

Observation

With the extensive research into this issue, I found a classloader issue that causes this problem. Further I noticed,

  • JAXB implementation is visible to ParallelWebappClassLoader, a classloader present in Tomcat server
  • Sometimes it is not visible to jdk internal classloaders like AppClassLoader,(Even though it was visible for many cases)

Solution

JAXBContext object is thread-safe (while marshaller/unmarshaller is not) and can be re-used once initiated. Therefore,

  1. I found a thread that works with ParallelWebappClassLoader (i.e that given thread's context class loader is ParallelWebappClassLoader) and created JAXBContext there and stored in a map for later usage
  2. Retrieved the stored JAXBContext whenever necessary (other threads that uses different class loaders) and carried out marshall/unmarshall tasks. That saved the day for me :)
Asanka Siriwardena
  • 871
  • 13
  • 18