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

JAXB not available on Tomcat 9 and Java 9/10

TLDR:在 Java 9 月 10 日,Tomcat 中的 Web 应用程序无法访问 JAXB,即使它的参考实现存在于class路径。

编辑:不,这不是 的副本 - 正如您可以通过 我尝试过的部分,我已经尝试了建议的解决方案。

情况

我们有一个在 Tomcat 上运行并依赖于 JAXB 的 Web 应用程序。在迁移到 Java 9 期间,我们选择添加 .

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

注:

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

这些是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 的 class 路径会有所帮助?

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[=15=](DefaultWmsRequestFactory.java:87) ~[cadenza-gis-webmapserver-7.7-SNAPSHOT.jar:7.6]

这显然是相同的 class,所以显然它已被两个 class 加载程序加载。我怀疑 the system class loader and the app's class loader,但为什么加载 JAXBContext 会被委托给系统 class 加载器一次而不是总是?看起来应用的 class 加载程序的委托行为似乎在程序运行时发生了变化。

添加模块

我真的不想添加 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 ...]

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

正在搜索错误

Searching Tomcat's bug database I found #62559。会不会是同样的错误?

将 JAR 添加到 Tomcat 的 lib

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

尝试以下及其依赖项。看到一个Maven repository for latest version.

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

它还包含 Java 服务加载器描述符。参见 Using JAXB in Java 9+

分析

首先是一些随机事实:

  • if not given a class loader, JAXBContext::newInstance will use the thread's context class loader 在查找 JAXB 实现时 - 即使您调用 newInstance(Class...) 也是这种情况(人们可能会错误地认为它使用提供的 class 实例加载器)
  • Tomcat 构建 a small class loader hierarchy 以将 Web 应用程序彼此分开
  • 通过不依赖模块 java.xml.bind,在 Java 9 中,JAXB classes 不会被 bootstrap 或系统 class 装载机

这是 Java 8 日发生的事情:

  • 我们没有将 class 加载程序传递给 JAXB(糟糕),因此它使用线程的上下文 class 加载程序
  • 我们的推测是 Tomcat 没有显式设置上下文 class 加载器,因此它最终将与加载 Tomcat 的相同:系统 class装载机
  • 这太棒了,因为系统 class 加载程序看到了整个 JDK,因此 JAXB 实现包含在其中

Java 9 进入 - 钢琴停止演奏,每个人都放下威士忌:

  • 我们添加了 ,因此它由网络应用程序的 class 加载程序加载
  • 就像 Java 8 一样,JAXB 搜索系统 class 加载器,但它看不到应用程序的加载器(只有相反的方向)
  • JAXB 找不到实现并崩溃

解决方案

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

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

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

备注

User curlals 提供了关键信息,但删除了他们的答案。我希望这不是因为我要求进行一些编辑。所有 credit/karma 都应该交给他们! @curlals:如果你恢复并编辑你的答案,我会接受并点赞。

TL;DR

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

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

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

调试问题我发现问题与以下事实有关:CompletableFutureRunner.

中使用了不同的 ClassLoader
// here Thread.currentThread().getContextClassLoader().getClass()
// returns org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader
return CompletableFuture.runAsync(() -> {
    // here returns jdk.internal.loader.ClassLoaders$AppClassLoader
});

第二个 ClassLoader 无法加载 JAXB 类。这种行为似乎只存在于 Java 9+ 中,实际上在 Java 9 ForkJoinPool.common() 返回一个 Executor 和你的主 ThreadClassLoader 之前],但在 Java 9 之后它 returns 是系统 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);
        }
    }
}

然后将使用该工厂的 Executor 传递给 runAsync() 方法:

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);
}

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

Implementation of JAXB-API has not been found

这是随机发生的,很难重现。幸运的是,我找到了一个系统环境,上面的错误是连续的,而其他环境则运行顺利。

观察

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

  • JAXB 实现对 ParallelWebappClassLoader 可见,classloader 存在于 Tomcat 服务器
  • 有时 它对 jdk 内部 class 加载程序不可见,例如 AppClassLoader,(尽管在许多情况下它是可见的)

解决方案

JAXBContext 对象是 thread-safe(而 marshaller/unmarshaller 不是)并且一旦启动就可以是 re-used。因此,

  1. 我找到了一个与 ParallelWebappClassLoader 一起工作的线程(即给定线程的上下文 class 加载器是 ParallelWebappClassLoader)并在那里创建了 JAXBContext 并存储在映射中供以后使用
  2. 必要时检索存储的 JAXBContext(使用不同 class 加载程序的其他线程)并执行 marshall/unmarshall 任务。这为我节省了一天:)