Java 9 前/后使用 JAXB (包括支持 javax.* 或 jakarta.*)

使用 Java 处理 Object 与 XML 之间的转换时 JAXB(Java Architecture for XML Binding) 仍然被广泛使用。但随着 Java 9 模块化后把 JAXB 从标准 JDK 中移除后,和 Java EE 8(Jakarta EE 8) 到 Jakarta EE 9 的变迁时命名空间由 javax.* 变成了 jakarta.*,我们在使用 JAXB 时需作出相应的适配。

本文分别使用 Java 1.8, 17,通过 Maven 插件 jaxb2-maven-plugin 的 xjc (从 xsd 文件生成 Java 类), 和如何切换 jakarta.* 命名空间,由此可给我们对使用了 JAXB 的项目升级 JDK 时指明方向。从而不致于因不了解每部分组件的具体功效而在 pom.xml 中胡乱配置,比如之前对 jaxb2-maven-plugin 插件本身配置了多余的 org.glassfish.jaxb:jaxb-xjc 和 org.glassfish.jaxb:jaxb-runtime 依赖,也未能理解 jaxb2-maven-plugin 与 org.glassfish.jaxb:jaxb-runtime 之间的版本对应关系。

 实验准备,创建简单的 Maven 项目,并在路径 src/main/xsd/ 下新建 Schema 文件 sample.xsd,内容为

如果再加上将要的测试代码 Client.java 的话,该 Maven 项目的目录结构就是

下面分别用在 Java 9 前后的版本进行测试,Java 9 之前选择最流行的 Java 8, 之后的以 JDK 17 作为代表。 

Java 1.8 中使用 xjc

Java 9 之前在 JDK 中内含 JAXB 组件,所以在 Java 8 中应用它很简单。

关于 jaxb2-maven-plugin xjc 的用法请参考官方的 Basic Examples - Java Generation. 我们多数时候遵循该插件的约定,可使对插件的配置最小化,如把 xsd 文件放在 src/main/xsd 中, 由 xsd 生成的 Java 源文件会在 target/generated-sources/jaxb 中, 该插件会自动把此目录自动作为源文件目录。xjc goal 会关联到 Maven 的 generate-sources Phase,所以 mvn compile 命令也就会自动生成 Java 文件和编译。

在 Maven 的 pom.xml 文件,只要在 build/plugins 中加上以下部分

现在执行 mvn 命令

mvn clean compile

在 target/generated-sources/jaxb/com/example/generation下生成 Item.java 和 ObjectFactory.java 文件

写一段代码进行测试 com/example/Client

执行该 Java 代码,得到输出

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<item>
    <title>Hello</title>
    <price>12.0</price>
</item>

如果查看当前目录的结构是

注意到我们这里所用的 jaxb2-maven-plugin 插件版本是 2.3.1, 在 MVN REPOSITORY jaxb2-maven-plugin 的最新版本有

升级 jaxb2-maven-plugin 插件到 2.4 或 2.5.0 也可正常工作. 但只有 2.3.1 可配置 <generateEpisode>false</generateEpisode> 而不用生成  *.episode 文件.

那么升级到 3.1.0 后会怎么样呢?

可正常执行 mvn genreate-sources,但生成的 Item.java 和 ObjectFactory.java 导入的包由 javax.* 变成了 jakarta.*, 由此可知 jaxb2-maven-plugin 的 3.1.0 是个分水岭,它是为 Jakarta EE 9 服务的。

Java 17 中使用 xjc

现在我们直接从 Java 8 跳到 Java 17, 已经知道这其中起关键性的阻碍是 Java 9 的模块化把 JAXB 从标准 JDK 的拿掉了。

如果我们对前面的代码(为避免编译错误先把 Client.java 代码注释掉),只是切换 Java 到版本 17, 继续用 jaxb2-maven-plugin:2.3.1,直接运行

mvn clean generate-sources

出现错误

Execution sample of goal org.codehaus.mojo:jaxb2-maven-plugin:2.3.1:xjc failed: A required class was missing while executing org.codehaus.mojo:jaxb2-maven-plugin:2.3.1:xjc: com/sun/codemodel/CodeWriter

升级插件到 2.4 后再次执行 mvn clean generate-sources, 得到新的错误

Caused by: java.lang.ClassNotFoundException: javax.activation.MimeTypeParseException

升级插件到 2.5.0 后 mvn clean generate-source 顺利通过, 但无法通过编译(mvn compile), 错误中有许多如下信息

package javax.xml.bind.annotation does not exist

因为 jaxb2-maven-plugin 生成的源文件 Item.java 中要导入的类如下

这些原本属性 JAXB 模块的类已由 Java 9 从核模块中移除了,要使用的话还须从外界重新引入, 也就是 JAXB Runtime

https://mvnrepository.com/search?q=jaxb-runtime 可找到几个相关的 JAXB Runtime, 如

  1. org.glassfish.jaxb:jaxb-runtime
  2. com.sun.xml.bind:jaxb-impl
  3. org.mule.glassfish.jaxb:jaxb-runtime
  4. javax.xml.bin:jaxb-api

等等,从多方面来看还是 org.glassfish.jaxb:jaxb-runtime 更流行,所以在 pom.xml 中加上依赖

再次执行

mvn clean compile

通过

在 org.glassfish.jaxb:jaxb-runtime 中实际是引入了 jakarta.xml.bind:jakarta.xml.bind-api 来支援 javax.xml.bind.* 的。当前 org.glassfish.jaxb:jaxb-runtime 比 2.3.9 更高的版本有  3.x 和 4.x,我们需把它的版本压制在 2.x 是因为 3.x 和  4.x 所关联的 jakarta.xml.bind:jakarta.xml.bind-api 的包名 javax.xml.bind.* 变成了 jakarta.xml.bind.* 了。

通过一系列的测试,我们可以总结出 jaxb2-maven-plugin 和 org.glassfish.jaxb:jaxb-runtime 的版本应如何搭配,支持项目中的 javax.* 或 jakarta.* 命名空间(如升级了 SpringBoot 到 3.2.x, 使用了 Tomcat 10 等)

继续使用 javax.* 命名空间

  1. jaxb2-maven-plugin: 2.x(2.5.0)
  2. org.glassfish.jaxb:jaxb-runtime: 2.x(2.3.9)

要采用新的 jarkata.* 命名空间

  1. jaxb2-maven-plugin: 3.x+(3.1.0)
  2. org.glassfish.jaxb:jaxb-runtime:3.x+(4.0.5)

完整的 pom.xml 的配置如下

jaxb2-plugin-version 和  jaxb-runtime-version 根据用 javax.* 还是 jakarta.* 包名来选择相应的版本。如果打算各路依赖都尽量往高的版本去升,那就选择用 jakarta.* 的命名空间吧。

要是使用了高版本的 Maven 插件, 最后把 Client.java 恢复回来,把其中的 javax.* 替换成 jakarta.* 就能正常工作了。

另外两个问题

JDK 21 与 Lombok 的兼容性

在 JDK 21 中在使用 jaxb2-maven-plugin 插件时,如果项目中使用了 Lombok 1.8.30 之前版本的话会出现出错

Fatal error compiling: java.lang.NoSuchFieldError:
Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field 'com.sun.tools.javac.tree.JCTree qualid'

把 Lombok 升级到与 JDK 21 兼容的 1.18.30 版本即可。

使用了 jaxb 命名空间的自定义 Schema

如果在自定义 xsd 文件中使用了 xmlns:jaxb 的话,执行 mvn generate-sources 也会出问题,例如有一个 user.xsd 文件

出错信息为

[WARNING] null [-1,-1] 
org.xml.sax.SAXParseException: No JAXB customization was detected in the schema but the prefix "jaxb" is used for other namespace URIs. If you did intend to use JAXB customization, make sure the namespace URI is "https://jakarta.ee/xml/ns/jaxb"
    at com.sun.tools.xjc.reader.xmlschema.parser.IncorrectNamespaceURIChecker.endDocument (IncorrectNamespaceURIChecker.java:70)
    at org.xml.sax.helpers.XMLFilterImpl.endDocument (XMLFilterImpl.java:485)
    at org.xml.sax.helpers.XMLFilterImpl.endDocument (XMLFilterImpl.java:485)
    at org.xml.sax.helpers.XMLFilterImpl.endDocument (XMLFilterImpl.java:485)
    at com.sun.tools.xjc.reader.internalizer.VersionChecker.endDocument (VersionChecker.java:100)

这可不仅仅是一个警告信息,实际上无法生成 UserRequest 类。我们需要按照信息中的提示,把前面 user.xsd 中 xs:schema 声明部分改成

这样就能正常了。

本文链接 https://yanbin.blog/java-9-jaxb-including-javax-jakarta/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
Perry Conn
Perry Conn
8 months ago

Your writing has a way of making even the most complex topics accessible and engaging. I'm constantly impressed by your ability to distill complicated concepts into easy-to-understand language.