这个包名变更到底是怎么回事
很多团队第一次听到”javax 要改成 jakarta”时,反应通常是困惑——好端端的包名为什么要动?事情要从 Oracle 把 Java EE 交给 Eclipse Foundation 说起。2017 年,Oracle 决定不再主导 Java EE 的后续发展,将其移交给了 Eclipse Foundation。但 Oracle 在移交协议里保留了一个条件:新的规范不能继续使用 javax 作为顶级包名。这个限制直接催生了 Jakarta EE 9 的核心变更——将所有 EE 相关 API 的包名从 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:tree 或 jdeps 找出所有引用了 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 类名引用。建议在日志告警中对 NoClassDefFoundError 和 ClassNotFoundException 提升关注级别。
依赖治理。迁移完成后,应该在新依赖引入流程中增加一条规则:禁止引入仍然使用 javax EE 命名空间的新依赖。可以在 CI 流水线中加一个检查脚本,扫描新增依赖是否包含 javax.servlet、javax.persistence 等包名。
团队知识更新。命名空间迁移对很多开发者来说是个认知盲区——他们可能不理解为什么要改、改了有什么影响。一次内部技术分享,解释 javax 和 jakarta 的来龙去脉、迁移过程中的典型问题和排查方法,能帮助团队在后续维护中减少重复踩坑。
Jakarta EE 的命名空间迁移本质上是一次生态层面的”断舍离”。它确实带来了短期的迁移成本和阵痛,但从长期来看,也让 Java 企业级开发的生态有了更清晰的发展方向。对于大多数仍在 javax 命名空间上的项目来说,与其被动等待依赖冲突爆发的那一天,不如趁着框架版本升级的窗口期,把这件事主动做掉。
原创文章,作者:,如若转载,请注明出处:https://fudengji.cn/article/311/