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

32
TLDR: 在Java 9/10上,Tomcat中的Web应用程序即使其参考实现存在于类路径上也无法访问JAXB。
编辑:不,这不是如何解决Java 9中的java.lang.NoClassDefFoundError:javax / xml / bind / JAXBException的重复-正如您可以从我尝试的部分看出,我已经尝试了建议的解决方案。
情况:我们有一个依赖于JAXB的Tomcat上运行的Web应用程序。在迁移到Java 9期间,我们选择添加JAXB参考实现作为常规依赖项。在使用IDE 嵌入式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文件添加到Tomcat的CLASSPATH

也许在setenv.sh中将JAR文件添加到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]

显然这是同一个类,因此很可能被两个类加载器加载。我怀疑系统类加载器和应用程序的类加载器,但为什么加载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中加载。

搜索错误

搜索Tomcat的错误数据库我找到#62559。那可能是相同的错误吗?

向Tomcat的lib添加JAR

按照Tomcat用户邮件列表上给出的建议,我将JAXB JAR添加到Tomcat的CATALINA_BASE/lib目录中,但在应用程序的lib文件夹中仍然遇到了相同的错误。

6个回答

18

分析

首先是一些随机事实:

以下是Java 8的情况:

  • 我们没有向JAXB传递类加载器(糟糕),因此它使用线程的上下文类加载器
  • 我们的猜测是Tomcat没有明确设置上下文类加载器,因此最终使用的是加载Tomcat的那个:系统类加载器
  • 这很好,因为系统类加载器看到整个JDK,因此包括其中的JAXB实现

Java 9进入 - 钢琴停止演奏,每个人都放下他们的苏格兰威士忌:

  • 我们将JAXB作为常规依赖项添加到了Web应用程序的类加载器中
  • 就像在Java 8上一样,JAXB搜索系统类加载器,但它无法看到应用程序的加载器(只能反过来)
  • JAXB未能找到实现并失败了

解决方案

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

  • 调用Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());,但那不是一个好主意
  • 创建上下文解析器,但这需要JAX-WS,感觉像是用另一个恶魔替换
  • 使用接受包的变体JAXBContext::newInstanceJava EE 7的Javadoc),它还需要一个类加载器并传递正确的加载器,虽然这需要进行一些重构

我们使用了第三个选项,并重构成了接受包的变体JAXBContext::newInstance。虽然繁琐,但解决了问题。

注意

用户curlals提供了关键信息,但删除了他们的答案。我希望这不是因为我要求进行一些编辑。所有的功劳/声望都应归给他们!@curlals:如果您恢复并编辑您的答案,我将接受并点赞。


@tom,我的回答不适用于你的具体情况,并不意味着它是错误的。这只是意味着我们的情况不同。你似乎忽略了那个对你有效的解决方案对我无效,而这个确实解决了我的问题。 - Nicolai Parlog
@Nicolai,您能否提供有关第三个选项解决方案的详细信息?我遇到了相同的问题(在使用parallelStream()时),第一种方法可以解决,但如果有其他选择,我更愿意避免将类加载器设置为线程。我的示例项目可以在此处找到:https://github.com/troger19/xml_classloader - troger19
@troger19:我已经添加了一个链接到我们最终使用的newInstance重载的Javadoc。如果您需要更多细节,您必须让我知道他们需要解释什么。 - Nicolai Parlog
@NicolaiParlog,您为什么认为设置TCCL是一个坏主意? - user1679671
很遗憾,我不记得我们是否有特定原因得出那个结论,或者它只是一般预防措施 - 正如你所看到的,类加载器的诡计变得非常复杂,如果可能的话,我更喜欢不去涉及它。 - Nicolai Parlog
显示剩余3条评论

9

请尝试以下内容及其依赖项。查看Maven仓库获取最新版本

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

此外,它还包含了Java服务加载器描述符。请参见在Java 9+中使用JAXB


不幸的是,它与_com.sun.xml.bind:jaxb-impl:2.3.0_一样,出现了完全相同的错误消息。 - Nicolai Parlog
太好了!谢谢!!!顺便说一下,这是使用jdk 10、tomcat 9、spring 5.3、hibernate 5.3等版本...只有在tomcat作为服务运行时才会出现CNF错误。如果从cmd行中以常规java应用程序的形式运行tomcat,则<dependency org="javax.xml.bind" name="jaxb-api" rev="2.3.0" />可以正常工作...为什么? - tom
只能在5分钟内编辑评论?哇,SO可以做得更好)))。至少我可以))))。....spring 5.3 -> spring 5 - tom

5

我在使用Spring Boot(版本2.2.6)内置Tomcat的代码中遇到了问题。在我的代码的特定部分,我使用了CompletableFuture。该代码在Java 8中运行良好,并通过相关的Java 12单元测试。当在Java 11或12中使用Tomcat执行应用程序时,问题才会出现。

调试问题后发现,问题与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,其ClassLoader是您的主线程,但在Java 9之后,它将返回具有系统ClassLoader的executor。
由于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);
}

1
谢谢你的帮助。这正是我所需要的。 - Natlum
1
非常感谢,救了我的命 :) - remes

1

TL;DR

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

我使用的Hibernate版本是5.2.10.Final,它们依赖于JAXB。然而,当我用Tomcat替换undertow时,这个依赖关系就丢失了。我找到了这个问题,但是没有一个答案真正解决了我的问题。当我发现jpa-model-gen是问题所在时,我很快意识到,它是唯一需要查找JAXB的Hibernate依赖项。将Hibernate版本更新到更高的版本就解决了我的问题。


0

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

找不到JAXB-API的实现

这个问题出现得很随机,很难复现。幸运的是,我找到了一个系统环境,在这个环境中上述错误一直存在,而在其他环境中它可以顺利工作。

观察

通过对这个问题进行广泛的研究,我发现了一个类加载器的问题导致了这个问题的出现。此外,我还注意到:

  • JAXB实现对于Tomcat服务器中的ParallelWebappClassLoader是可见的
  • 有时候它对于jdk内部的类加载器(即使它在许多情况下是可见的)是不可见的

解决方案

JAXBContext对象是线程安全的(虽然marshaller/unmarshaller不是),一旦初始化就可以重复使用。因此,

  1. 我找到了一个可以与ParallelWebappClassLoader一起使用的线程(即给定线程的上下文类加载器是ParallelWebappClassLoader),并在那里创建了JAXBContext,并将其存储在映射中以供以后使用。
  2. 每当需要时(使用不同类加载器的其他线程),检索存储的JAXBContext并执行marshall/unmarshall任务。这为我解决了问题 :)

0
我已经为此苦苦挣扎了几天。感谢您提供的三个解决方案。然而,我的要求是不改变任何源代码。我找到了一个非常简单的解决方案。基本上,在我将war文件部署到Tomcat10应用服务器之后,我将jaxb-impl.2.2.6的jar文件复制到WEB-INF\classes目录下。然后使用命令"jar -xvf jaxb-impl.2.2.6"从那里解压缩jar文件,然后喜出望外,我5天的噩梦终于结束了,我以前的JAXB项目在Tomcat 10上成功运行起来了!
所以,停止演奏的钢琴又开始奏响了。我喜欢这个解决方案,因为我不需要改变任何代码。作为改进的一部分,我现在将在部署war文件时解压缩jar文件。

根据目前的写法,你的回答不够清晰。请编辑以添加更多细节,以帮助其他人理解这如何回答所提出的问题。你可以在帮助中心找到更多关于如何撰写好的回答的信息。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接