Jakarta EE 迁移指南:从 javax 到 jakarta 包名的完整升级路径

这个包名变更到底是怎么回事

很多团队第一次听到”javax 要改成 jakarta”时,反应通常是困惑——好端端的包名为什么要动?事情要从 Oracle 把 Java EE 交给 Eclipse Foundation 说起。2017 年,Oracle 决定不再主导 Java EE 的后续发展,将其移交给了 Eclipse Foundation。但 Oracle 在移交协议里保留了一个条件:新的规范不能继续使用 javax 作为顶级包名。这个限制直接催生了 Jakarta EE 9 的核心变更——将所有 EE 相关 API 的包名从 javax.* 迁移到 jakarta.*。

Jakarta EE 迁移指南:从 javax 到 jakarta 包名的完整升级路径

这里有一个容易被忽略的关键细节:并非所有 javax 包都发生了变更。Java SE 自带的 javax 包(比如 javax.sql、javax.net、javax.crypto)完全不受影响,它们仍然属于 JDK 的一部分。真正发生变更的,是那些属于 Java EE / Jakarta EE 规范的 API,包括但不限于 Servlet、JPA、JAX-RS、Bean Validation、CDI、JMS、JAXB、JAX-WS 等。如果你在迁移时一刀切地全局替换 javax 为 jakarta,Java SE 的包也会被错误改掉,编译直接报错。

这个变更在 Jakarta EE 9 中正式落地,此后 Jakarta EE 10、11 都延续了 jakarta 命名空间。而 Spring Framework 6 / Spring Boot 3、Tomcat 10+、Hibernate 6.x、Jetty 11+、JBoss EAP 8 等主流运行时和框架也纷纷跟进,全面切换到 jakarta.* 包名。这意味着,如果你的项目依赖了上述任何一条升级链路,迁移就成了绕不开的环节。

哪些场景会逼着你去迁移

迁移需求通常不是主动选择的结果,而是被动的连锁反应。以下几种典型场景,是实际项目中最常见的触发点:

  • 框架版本升级:比如团队决定从 Spring Boot 2.x 升级到 3.x,Spring Framework 6 强制要求 Jakarta EE 9+,所有 javax.* 的 EE API 导入必须改为 jakarta.*。
  • 应用服务器升级:从 Tomcat 9 迁移到 Tomcat 10,从 JBoss EAP 7 迁移到 EAP 8,从 WebLogic 14c 迁移到新版本——这些服务器的 Servlet 容器已经切换到 jakarta 命名空间。
  • 安全漏洞或合规要求:旧版框架的 CVE 修复只在 jakarta 命名空间的新版本中提供,团队被迫升级才能拿到安全补丁。
  • 依赖冲突:引入了新的第三方库,它传递依赖了 jakarta.* 的 API,而你的项目还在用 javax.*,导致类加载冲突或 ClassNotFoundException。

真正让人头疼的往往不是自己写的代码——IDE 全局替换一下 import 也就几分钟的事。麻烦的是那些你看不到源码的第三方依赖:你的项目引入了一个三年没更新的库,它的 class 文件里硬编码了 javax.servlet 的引用,而你的新容器只提供 jakarta.servlet。运行时直接抛 NoClassDefFoundError,排查起来非常折磨人。

迁移的两种路径:源码替换 vs 字节码转换

面对迁移,本质上只有两条路可以走。选择哪条,取决于你对源码的控制力和项目的复杂度。

维度 源码级迁移 字节码转换(Bytecode Transformation)
适用条件 有完整源码,能重新编译部署 无源码或源码不可控(如二进制 JAR 依赖)
典型工具 IntelliJ IDEA 迁移工具、OpenRewrite Eclipse Transformer、Red Hat Migration Toolkit
迁移精度 高,可以逐文件检查和调整 中等,依赖规则映射文件的覆盖度
后续维护 源码直接就是 jakarta 版本,后续无额外成本 每次依赖更新可能需要重新转换
风险 需要处理 API 行为变更(如 CDI 4.0 默认发现模式变化) 转换规则不覆盖的边缘场景可能静默失败
适用团队 有源码主权的中型以上团队 依赖大量闭源 JAR 的企业级项目

大多数团队最终走的是源码级迁移,因为这样最干净、最可控。但对于那些包含大量闭源第三方 JAR 的企业项目——比如银行、电信行业里常见的场景——字节码转换往往是唯一可行的方案。

源码迁移:IDE 工具比手动替换靠谱得多

前面提过,直接全局搜索替换 javax → jakarta 是会出事的。IntelliJ IDEA 从 2021.3 版本开始内置了专门的迁移工具,它维护了一份精确的包名映射表,知道哪些 javax 包应该被替换为 jakarta,哪些应该保持不动。

使用方式很直接:

// IntelliJ IDEA 迁移路径:
// Refactor → Migrate Packages and Classes → Java EE to Jakarta EE
//
// 工具会自动识别以下映射(部分示例):
// javax.servlet          → jakarta.servlet
// javax.persistence      → jakarta.persistence
// javax.validation       → jakarta.validation
// javax.ws.rs            → jakarta.ws.rs
// javax.enterprise        → jakarta.enterprise
// javax.jms              → jakarta.jms
// javax.xml.bind          → jakarta.xml.bind
//
// 而这些包不会被替换(Java SE 原生):
// javax.sql              → 保持不变
// javax.net              → 保持不变
// javax.crypto           → 保持不变
// javax.transaction.xa   → 保持不变(注意:javax.transaction 的
//                          EE 部分会迁移,但 xa 子包不迁移)

这里有一个特别容易踩的坑:javax.transaction 这个包是分裂的。它的 EE 部分(javax.transaction.Transactional 等注解)会迁移到 jakarta.transaction,但 javax.transaction.xa 包属于 Java SE,不会迁移。如果你用全局替换,xa 包也会被改掉,编译就挂了。IDE 的迁移工具知道这个区别,手动替换则很容易出错。

IntelliJ 的迁移工具支持按模块范围执行,可以先在一个非核心模块上跑一遍预览,检查 diff 确认无误后再全量执行。对于大型多模块项目,分模块迁移是一个更安全的策略。

不只是 import:这些地方也藏着 javax 引用

很多团队做完 import 替换后以为迁移完成了,结果启动时各种报错。原因在于 javax 的引用远不止 Java 源文件里的 import 语句。以下这些位置同样需要处理:

  • 配置文件persistence.xml 中的命名空间 URI、web.xml 中的 schema 声明、beans.xml 的 version 属性,都可能引用 javax 的 schema 地址。
  • 系统属性:一些 EE 规范定义的系统属性名以 javax 开头,比如 javax.persistence.provider,迁移后应改为 jakarta.persistence.provider
  • ServiceLoader 文件:通过 META-INF/services/javax.xxx 注册的 SPI 实现类,文件名也需要从 javax 改为 jakarta。
  • 注解处理器和反射:代码中如果通过字符串硬编码引用了类名(比如 Class.forName("javax.servlet.Servlet")),这些字符串也需要手动更新。

以 persistence.xml 为例,迁移前后的差异:

<!-- 迁移前 -->
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
               http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
             version="2.2">

<!-- 迁移后 (Jakarta EE 9+) -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
               https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">

这类文件 IDE 迁移工具不一定能自动覆盖,需要在迁移检查清单中单独列出。

第三方依赖的传递性问题

这是整个迁移过程中最容易低估的难点。你的项目代码迁移完了,编译也通过了,但运行时抛出 NoClassDefFoundError: javax/servlet/http/HttpServlet。原因在于某个第三方依赖的 JAR 里仍然引用了 javax 命名空间。

典型场景:一个中等规模的电商后台系统,用了 MyBatis-Plus 做数据访问层,用了 SpringDoc 生成 API 文档,还引入了一个老旧的 Excel 导出库。这三个依赖中:

  • MyBatis-Plus 在 3.5.3.1+ 版本才完全兼容 jakarta 命名空间
  • SpringDoc 需要升级到 v2.5.0+ 才支持 Spring Boot 3 / jakarta
  • 那个 Excel 导出库已经不再维护,内部硬编码了 javax.xml.bind 的引用

对于前两种情况,升级版本就行。但第三种情况——依赖已经停止维护——就只能通过 Eclipse Transformer 做字节码转换,或者寻找替代方案。

排查这类问题的有效手段是使用 Maven 的依赖树分析:

# 查看哪些依赖仍然引用了 javax 的 EE API
mvn dependency:tree -Dincludes=javax.servlet:*,javax.persistence:*

# 或者使用 jdeps 分析字节码层面的依赖
jdeps --module-path libs --recursive --print-module-deps your-app.jar

# 更彻底的方式:直接在 JAR 中搜索 javax 引用
find ~/.m2/repository -name "*.jar" -exec sh -c 
  'unzip -l "$1" | grep -q "javax/servlet" && echo "$1"' _ {} ;

这一步排查做得越细,后面运行时的惊喜就越少。建议在迁移开始前就先跑一遍依赖扫描,形成一份”需要处理的第三方依赖清单”,按优先级逐个处理。

框架适配的版本对齐问题

迁移到 jakarta 命名空间不只是改包名这么简单,它通常伴随着一整套框架版本的对齐升级。因为 jakarta 命名空间是在各规范的新版本中引入的,你不能用旧版本的 API 语法配合新版本的包名。

组件 javax 时代版本 jakarta 时代版本 关键变更点
Servlet API 4.0 (javax) 5.0+ (jakarta) 包名迁移,API 兼容
JPA / Hibernate 2.2 / 5.x 3.0+ / 6.x 包名迁移,Hibernate 6 有查询语法变更
Bean Validation 2.0 (javax) 3.0+ (jakarta) 包名迁移,API 基本兼容
CDI 2.0 (javax) 4.0 (jakarta) 默认 Bean 发现模式从 all 改为 annotated
Tomcat 9.x 10.x+ Servlet 容器全面切换 jakarta
Spring Boot 2.x 3.x JDK 17+,jakarta 全量迁移

其中 CDI 4.0 的变更尤其需要注意。在 Jakarta EE 9 之前,空的 beans.xml 文件意味着所有类都会被当作 CDI Bean 发现和处理。但从 CDI 4.0(Jakarta EE 10)开始,默认行为变成了 annotated 模式——只有标注了 CDI 限定注解的类才会被发现。如果你的项目中有依赖空 beans.xml 全量扫描的代码,升级后这些 Bean 会静默消失,表现为注入点为 null,排查起来非常困难。

字节码转换:Eclipse Transformer 实战要点

当你面对一个无法获取源码的 JAR 依赖时,Eclipse Transformer 是目前最成熟的字节码转换工具。它的工作原理是在 class 文件层面做包名替换,生成一个全新的 JAR 文件。

使用方式相对简单,但需要理解它的规则文件机制:

# 基本用法:转换单个 JAR 文件
java -jar eclipse-transformer.jar 
  --input original-lib.jar 
  --output transformed-lib.jar 
  --rules jakarta-rules.properties 
  --selection jakarta-selection.properties

# 在 Maven 构建中集成(通过插件)
<plugin>
  <groupId>org.eclipse.transformer</groupId>
  <artifactId>transformer-maven-plugin</artifactId>
  <version>0.5.0</version>
  <executions>
    <execution>
      <goals><goal>transform</goal></goals>
    </execution>
  </executions>
</plugin>

Eclipse Transformer 内置了一套默认的 Jakarta EE 规则映射,覆盖了大部分标准 EE API 的包名替换。但它的局限在于:如果你的依赖使用了非标准的扩展机制或者自定义的类加载策略,转换可能无法覆盖,需要手动补充规则文件。

另一个选择是 OpenRewrite,它走的是另一条路线——通过 recipes(规则配方)在构建时自动修改源码。OpenRewrite 的 JavaxToJakarta recipe 不仅能处理 import 语句,还能处理配置文件、注解属性中的字符串引用。对于 Maven 项目,集成方式如下:

<plugin>
  <groupId>org.openrewrite.maven</groupId>
  <artifactId>rewrite-maven-plugin</artifactId>
  <version>5.20.0</version>
  <configuration>
    <activeRecipes>
      <recipe>org.openrewrite.java.migrate.jakarta.JavaxToJakarta</recipe>
    </activeRecipes>
  </configuration>
  <dependencies>
    <dependency>
      <groupId>org.openrewrite.recipe</groupId>
      <artifactId>rewrite-migrate-java</artifactId>
      <version>2.0.0</version>
    </dependency>
  </dependencies>
</plugin>

OpenRewrite 的优势是可审计——它会生成一份详细的变更报告,告诉你每个文件做了什么修改,方便 review。但它的执行速度比 IDE 工具慢一些,大型项目可能需要几分钟。

一套分阶段迁移策略

基于上面的分析,建议按照以下阶段推进迁移工作,降低风险:

阶段一:评估与盘点。在全量迁移前,先完成依赖扫描和影响面评估。用 mvn dependency:treejdeps 找出所有引用了 javax EE API 的依赖,列出版本清单,确认每个依赖是否有 jakarta 兼容版本。对于没有兼容版本的依赖,提前制定替代方案或字节码转换计划。

阶段二:环境与构建链升级。先升级 JDK(Spring Boot 3 要求 JDK 17+),升级 Maven/Gradle 版本,升级 IDE 版本确保支持新语法和迁移工具。在一个独立分支上操作,不要直接动主干。

阶段三:源码与配置迁移。在 IDE 中运行 Jakarta EE 迁移工具,逐模块执行并 review diff。然后手动检查 persistence.xml、web.xml、beans.xml 等配置文件中的命名空间引用。用全文搜索确认没有遗漏的 javax 字符串。

阶段四:依赖升级与对齐。将所有第三方依赖升级到 jakarta 兼容版本。这一步和阶段三可以交叉进行,但建议先处理核心框架(Spring、Hibernate、Servlet API),再处理工具类库。

阶段五:编译验证与测试。编译通过不等于迁移成功。重点要跑集成测试和端到端测试,因为很多运行时问题只有在容器启动、Bean 注入、HTTP 请求处理时才会暴露。特别关注 CDI Bean 发现行为变化导致的注入失败。

经验提醒:如果项目规模较大,不要试图在一个 sprint 内完成全部迁移。按模块拆分、分批上线、保留回滚窗口,是更稳妥的做法。

迁移完成后需要持续关注什么

迁移完成并不是终点,有几个后续事项值得长期关注:

监控运行时异常。迁移后的前两周是问题高发期,尤其是反射调用、动态代理、序列化等场景中可能残留的 javax 类名引用。建议在日志告警中对 NoClassDefFoundErrorClassNotFoundException 提升关注级别。

依赖治理。迁移完成后,应该在新依赖引入流程中增加一条规则:禁止引入仍然使用 javax EE 命名空间的新依赖。可以在 CI 流水线中加一个检查脚本,扫描新增依赖是否包含 javax.servlet、javax.persistence 等包名。

团队知识更新。命名空间迁移对很多开发者来说是个认知盲区——他们可能不理解为什么要改、改了有什么影响。一次内部技术分享,解释 javax 和 jakarta 的来龙去脉、迁移过程中的典型问题和排查方法,能帮助团队在后续维护中减少重复踩坑。

Jakarta EE 的命名空间迁移本质上是一次生态层面的”断舍离”。它确实带来了短期的迁移成本和阵痛,但从长期来看,也让 Java 企业级开发的生态有了更清晰的发展方向。对于大多数仍在 javax 命名空间上的项目来说,与其被动等待依赖冲突爆发的那一天,不如趁着框架版本升级的窗口期,把这件事主动做掉。

原创文章,作者:,如若转载,请注明出处:https://fudengji.cn/article/311/

(0)
上一篇 1天前
下一篇 1天前

相关推荐