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

Nic*_*lai 19 java tomcat java-9 java-10

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

编辑:不,这不是如何解决java.lang.NoClassDefFoundError:Java 9中的javax/xml/bind/JAXBException的副本- 正如您在"我尝试过的内容"部分所述,我已经尝试了所提出的解决方案.

情况

我们有一个在Tomcat上运行的Web应用程序,它依赖于JAXB.在我们迁移到Java 9期间,我们选择将JAXB参考实现添加为常规依赖项.

具有嵌入式Tomcat的IDE启动应用程序时,一切正常,但在真正的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 ...]
Run Code Online (Sandbox Code Playgroud)

注意:

在模块路径或类路径上找不到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
Run Code Online (Sandbox Code Playgroud)

这里发生了什么?

我尝试了什么

将JAR添加到Tomca的 CLASSPATH

也许将JAR添加到Tomcat的类路径中会有帮助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
Run Code Online (Sandbox Code Playgroud)

不:

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]
Run Code Online (Sandbox Code Playgroud)

这显然是同一个类,显然它已被两个类加载器加载.我怀疑系统类加载器和应用程序的类加载器,但为什么加载JAXBContext被委托给系统类加载器一次但不总是?在程序运行时,几乎看起来app的类加载器的委托行为发生了变化.

添加模块

我真的不想添加java.xml.bind,但我还是尝试了将它添加到catalina.sh:

JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-modules=java.xml.bind"
Run Code Online (Sandbox Code Playgroud)

但是也不起作用:

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 ...]
Run Code Online (Sandbox Code Playgroud)

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

搜索错误

搜索Tomcat的bug数据库,我找到了#62559.这可能是同样的错误吗?

将JAR添加到Tomcat中 lib

根据Tomcat用户邮件列表给出的建议,我将JAXB JAR添加到Tomcat的CATALINA_BASE/lib目录中,但是得到了与应用程序的lib文件夹中相同的错误.

Nic*_*lai 10

分析

首先是一些随机事实:

所以这就是Java 8上发生的事情:

  • 我们不会将类加载器传递给JAXB(oops),因此它使用线程的上下文类加载器
  • 我们的猜想是Tomcat没有显式设置上下文类加载器,所以最终会加载Tomcat:系统类加载器
  • 这是花花公子,因为系统类加载器看到整个JDK,因此包含其中的JAXB实现

Java 9进入 - 钢琴停止播放,每个人都放下苏格兰威士忌:

  • 我们将JAXB添加为常规依赖项,因此它由Web应用程序的类加载器加载
  • 就像在Java 8上一样,JAXB搜索系统类加载器,但是那个人看不到应用程序的加载器(只是反过来)
  • JAXB无法找到实现并且无法实现

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

  • 打电话Thread.getCurrentThread().setContextClassLoader(this.getClass().getClassLoader());但这不是一个好主意
  • 创建一个上下文解析器,但这需要JAX-WS,这就像用另一个邪恶替换一个邪恶
  • 使用接受包的接受变体JAXBContext::newInstance(来自Java EE 7的Javadoc),它也接受类加载器并传递正确的加载器,尽管这需要一些重构

我们使用了第三个选项,并针对包接受变体进行了重构JAXBContext::newInstance.琐碎的工作,但修复了问题.

注意

用户curlals提供了关键信息,但删除了他们的答案.我希望不是因为我要求进行一些编辑.所有的信用/业力应该归他们所有!@curlals:如果你恢复并编辑你的答案,我会接受并赞成它.


Phi*_*all 6

尝试以下及其依赖项.查看Maven存储库以获取最新版本.

<dependency>
  <groupId>org.glassfish.jaxb</groupId>
  <artifactId>jaxb-runtime</artifactId>
  <version>2.3.0.1</version>
</dependency>
Run Code Online (Sandbox Code Playgroud)

它还包含Java Service Loader描述符.请参阅在Java 9+中使用JAXB


xon*_*nya 5

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

调试问题我发现问题与'sClassLoader内部使用了不同的事实有关。CompletableFutureRunner

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

第二个ClassLoader是无法加载 JAXB 类。这种行为似乎仅在 Java 9+ 中出现,实际上在 Java 9ForkJoinPool.common()返回Executor带有ClassLoadermain 的a 之前Thread,但在 Java 9 之后它返回带有 system 的执行器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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后将Executorusing 该工厂传递给该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);
}
Run Code Online (Sandbox Code Playgroud)