JAXB在Tomcat 9和Java 9/10上不可用

时间:2018-07-25 12:21:53

标签: java tomcat java-9 java-10

TLDR :在Java 9/10上,Tomcat中的Web应用程序无法访问JAXB,即使其引用实现位于类路径中。

编辑:不,这不是How to resolve java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException in Java 9的副本-正如您在我尝试过的内容部分所指出的那样,我已经尝试了建议的解决方案

情况

我们有一个可在Tomcat上运行并依赖JAXB的Web应用程序。在迁移到Java 9的过程中,我们选择添加the JAXB reference implementation as a regular dependency

从IDE with embedded Tomcat启动应用程序时,一切正常,但是在真正的Tomcat实例上运行它时,出现此错误:

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 ...]

注意:

  

在模块路径或类路径上找不到JAXB-API的实现。

这些是webapps/$app/WEB-INF/lib中的相关文件:

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

这是怎么回事?

我尝试过的

将JAR添加到Tomca的CLASSPATH

也许可以将JAR添加到setenv.sh中的Tomcat的类路径中?

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

不是:

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]

这显然是同一类,因此显然它已经由两个类加载器加载了。我怀疑the system class loader and the app's class loader,但是为什么将一次加载JAXBContext委派给系统类加载器,但并非总是如此?程序运行时,看起来好像应用程序的类加载器的委派行为发生了变化。

添加模块

我真的不想添加 java.xml.bind ,但是无论如何我都通过将其添加到catalina.sh来进行了尝试:

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

但是也不起作用:

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 ...]

除了具有不同的类和堆栈跟踪之外,这与之前发生的情况是一致的:类JAXBContextImpl被加载了两次,一次是从 java.xml.bind 加载的(必须已系统类加载器)和另外一次(我假设应用程序的加载器来自JAR)。

搜索错误

Searching Tomcat's bug database我发现了#62559。会是同样的错误吗?

将JAR添加到Tomcat的lib

advice given on the Tomcat user mailing list之后,我将JAXB JAR添加到Tomcat的CATALINA_BASE/lib目录中,但是出现了与应用程序的lib文件夹相同的错误。

5 个答案:

答案 0 :(得分:7)

分析

首先是一些随机事实:

  • 如果不是given a class loaderJAXBContext::newInstance在寻找JAXB实现时将使用the thread's context class loader-即使您调用newInstance(Class...),情况也会如此(可能会错误地认为它使用了提供的类实例的加载程序)
  • Tomcat构建a small class loader hierarchy来将Web应用程序彼此分开
  • 通过不依赖模块 java.xml.bind ,在Java 9中,bootstrap或系统类加载器未加载JAXB类

这就是Java 8的情况:

  • 我们没有将类加载器传递给JAXB(哎呀),因此它使用线程的上下文类加载器
  • 我们的猜测是Tomcat没有显式设置上下文类加载器,因此最终将与加载Tomcat相同:系统类加载器
  • 这很花哨,因为系统类加载器可以看到整个JDK,因此可以看到其中包含的JAXB实现。

Java 9进入-钢琴停止弹奏,所有人放下苏格兰威士忌:

  • 我们添加了JAXB as a regular dependency,因此它是由网络应用的类加载器加载的。
  • 不过,就像在Java 8上一样,JAXB会搜索系统类加载器,并且看不到应用程序的加载器(仅反过来)
  • JAXB无法找到实现,并且大吃一惊

解决方案

解决方案是确保JAXB使用正确的类加载器。我们知道三种方式:

  • 致电Thread.getCurrentThread().setContextClassLoader(this.getClass().getClassLoader());,但这并不是一个好主意
  • 创建a context resolver,但这需要JAX-WS,感觉就像用另一种邪恶代替了
  • 使用JAXBContext::newInstanceJavadoc from Java EE 7)的包接受变体,它也需要一个类加载器并传递正确的加载器,尽管这需要一些重构

我们使用了第三个选项,并将其重构为JAXBContext::newInstance的可接受包的变体。艰苦的工作,但解决了这个问题。

注意

User curlals提供了关键信息,但删除了他们的答案。我希望不是因为我要求进行一些编辑。所有的信用/业力都应归功于他们! @curlals:如果您恢复并编辑答案,我会接受并支持。

答案 1 :(得分:5)

尝试以下操作及其依赖项。参见Maven repository for latest version

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

它还包含Java Service Loader描述符。参见Using JAXB in Java 9+

答案 2 :(得分:1)

TL; DR

一个对我有用的简单解决方案就是升级Hibernate版本。

我在5.2.10.Final版本中使用了Hibernate,它们依赖于JAXB。但是,当我用Tomcat替换undertow时,该依赖性消失了。我找到了这个问题,但是没有一个答案能真正解决我的问题。当我发现jpa-model-gen是我很快意识到的问题时,唯一依赖Hibernate的依赖就是寻找JAXB。将休眠版本更新到更高版本可以解决我的问题。

答案 3 :(得分:1)

我在使用CompletableFuture的代码的特定部分中使用带有嵌入式Tomcat的Spring Boot(版本2.2.6)时遇到了这个问题。该代码与Java 8以及在Java 12中通过的相关单元测试完美配合。仅当使用Java 11或12在Tomcat中执行该应用程序时,该问题才会出现。

调试问题,我发现问题与以下事实有关:在ClassLoader的{​​{1}}中使用了不同的CompletableFuture

Runner

第二个// here Thread.currentThread().getContextClassLoader().getClass() // returns org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader return CompletableFuture.runAsync(() -> { // here returns jdk.internal.loader.ClassLoaders$AppClassLoader }); 无法加载JAXB类。此行为似乎仅在Java 9+中才存在,实际上是在Java 9 ClassLoader返回带有主ForkJoinPool.common()的{​​{1}}的{​​{1}}之前,但是在Java 9之后,它返回了系统Executor的执行者。

由于ClassLoader方法接受Thread作为第二个参数,因此可以在代码中设置所需的ClassLoader。这是一个可能的解决方案的示例。

首先,定义一个适当的CompletableFuture.runAsync()

Executor

然后使用该工厂将Executor传递给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);
        }
    }
}

答案 4 :(得分:0)

我在使用JAXB时也遇到过类似的问题,即

未发现JAXB-API的实现

随机发生,很难复制。幸运的是,我发现了一个系统环境,上面的错误是连续的,而其他环境则运行正常。

观察

通过对该问题的广泛研究,我发现了导致此问题的 classloader 问题。我进一步注意到,

  • JAXB实现对 ParallelWebappClassLoader (Tomcat服务器中存在的类加载器)可见
  • 有时对于诸如AppClassLoader之类的jdk内部类加载器来说是不可见的(尽管在很多情况下它是可见的)

解决方案

JAXBContext对象是线程安全的(而marshaller / unmarshaller则不是),一旦启动,便可以重新使用。因此,

  1. 我找到了一个与ParallelWebappClassLoader配合使用的线程(即给定线程的上下文类加载器为ParallelWebappClassLoader),并在那里创建了JAXBContext并存储在地图中供以后使用
  2. 在必要时(其他使用不同类加载器的线程)检索存储的JAXBContext并执行编组/解组任务。那为我节省了一天:)