WAR 在新容器报类找不到,订单 SQL 却完全没变

遗留采购应用由 Java EE 8 的 Servlet、REST、CDI 和事务 API 编译,打包后部署到使用 Jakarta EE 11 的运行时。一些源文件引用 javax.servlet.*,新容器的企业 API 在 jakarta.servlet.*;旧 WAR 即使有相同 URL 和 SQL,也不能据此推断二进制兼容。有人用全局字符串替换把 javax.sql.DataSource 改成 jakarta.sql.DataSource,编译立刻失败:JDBC 的 javax.sql 属于 Java SE,迁移企业 API 时不改包名。迁移验收应核对同一采购用例在旧/新环境的请求、事务、身份和失败契约,而非仅以编译结果定案。

E07 必须从独立旧工程开始:冻结 Java EE 8 应用服务器具体发行版、JDK 发行版/补丁、Maven 与每项 API 依赖,保存原始 WAR 哈希、启动配置、数据库迁移版本和针对合成数据的正常/失败基线。本文不创建或修改这个旧工程;现有 examples/javaee-enterprise/ 原本就是 JDK 21/Jakarta EE 11 工程,不能把它改回 javax.* 来伪造“旧系统已迁移”。资料指出 Java EE 8 与 Jakarta EE 8 的企业 API 仍使用 javax.*;Jakarta EE 9 对相应企业 API 迁为 jakarta.*,这也是类签名、反射名、描述符和二进制依赖必须一起检查的原因。Jakarta EE 11 Platform 最低 Java SE 17,本系列选 JDK 21;不能让原本基于 Java 8 的旧测试在 JDK 21 上能编译,就断言它已经符合 EE 11 的所有行为变化。

迁移清单必须区分归属

旧系统的依赖树至少按三组列出:Java SE 不动、Java EE 8 平台 API 与实现、第三方库通过传递依赖带进 WAR。只有第一组能仅凭 JDK 文档定为保留;第二组需按目标 Jakarta 规范升级;第三组需核查是否真的提供 Jakarta 兼容发行版,而不是靠顶层依赖换名掩盖内部旧 API。若 WAR 中同时带有旧 javax.servlet API 与目标容器自己的 jakarta.servlet,两个类名可能都存在却不能在同一 Servlet 接口调用链互换。排查应从报错的运行时实际类来源进入,而不从 IDE 里能跳转到哪个 jar 作推断。

XML 与反射的迁移易漏:旧 web.xml 引用的 schema、旧 beans.xml 的发现方式、旧 JPA persistence 单元、ServiceLoader 提供者文件、拦截器注解和反射中写死的完全限定名都不会因为 javac 更新 import 自动修改。服务器端 JNDI 逻辑名可以保持相同,但其创建与密码来源需要在新服务器重新绑定。日志配置或生产监控中按旧异常名匹配告警的规则也需重查;即使 SQL 行正确,故障报警因类别改名而失效,运维恢复路径仍可能退化。

先画出旧工程模块依赖图:应用 WAR 内含哪些 API 包、第三方库、驱动、服务提供者和资源适配器;服务器自己提供哪些实现;部署时谁先装载类。企业 API 如旧 javax.servlet、javax.ws.rs、javax.enterprise.context、javax.transaction 和旧安全 API 应按目标规范和依赖兼容性迁移;Java SE 的 javax.sql、javax.naming 等不因名称相似就替换。不要对所有 javax.xml.* 作正则批量修改:某些属于 Java SE,某些与旧独立 XML 技术相关,应逐项按所属规范确认。JPA 若出现在旧采购应用,也应迁移 javax.persistence 到目标 jakarta.persistence,但它的映射与抓取规则由独立先行JPA 规范与 Provider解释;本选修只核对企业应用调用和依赖边界,不在 JDBC 主线重讲 ORM。

1
2
3
4
5
6
7
8
9
10
11
12
package blog.javaee.migration;

import jakarta.annotation.Resource;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import javax.sql.DataSource;

@WebServlet("/servlet/migration-check")
public class MigrationCheckServlet extends HttpServlet {
@Resource(lookup = "jdbc/Procurement")
private DataSource source;
}

这段代码只展示目标版混合包名的合法边界,不是旧版本源代码,也未提供可调用的 doGet 或业务断言。不能根据注入字段类型为 DataSource 就断言部署后 JNDI 资源已经存在、数据库可用、事务自动管理或角色约束被保留。实际迁移时重新检查 servlet 映射、bean discovery、注解扫描、REST 应用路径、安全声明、EJB 方法属性与事务拦截的受管入口;即使代码层包名全部改了,web.xml、beans.xml、persistence.xml、供应商资源配置中的 XML namespace/schemaLocation、filter 映射、反射字符串与服务加载文件也可能仍指向旧 API。

从编译通过到行为等价隔着几个失败窗口

先行旧环境基线并不要求把遗留系统直接暴露公网。可在隔离服务器用合成采购员、两租户和本地 PostgreSQL 实验副本回放合同;基线不能只保留一份 mvn test 的绿灯。要保存 URL、请求体摘要、匿名和越权响应、用例提交后的申请/明细/订单行,以及故意让明细写入失败后的回滚行集合。然后确保新系统选择的是可比的库/驱动与 schema,而不是旧版连接 jpa_lab、新版连接 javaee_lab 却用不同数据量比较“是否成功”。数据迁移和 API 迁移是两项任务,必要时单独演练 schema expand/contract,不能归因混淆。

历史旧工程若使用容器 FORM 登录,新系统需要核查登录页、回调、会话轮换、注销和 CSRF,而不是只看“输入密码后页面显示欢迎”。为防止默认开放,先列出方法级/URL 级角色与对象权限矩阵;在新部署上用匿名、申请人、审批员分别打同一已提交申请,核对状态码与数据库变化。客户端伪造 X-Lab-Tenant 能批准别人订单,属于业务授权未迁移,不能用“服务已启动”掩盖。身份存储升级后的密码散列校验也要设计安全迁移,不能把旧凭据或散列明文记录到测试报告。

第一层是干净构建和依赖树。旧版 API 若被装进新 WAR,可能和容器提供的 Jakarta 类共存却在调用边界发生 ClassNotFoundException/NoSuchMethodError;只看 IDE 标绿不够,要检查 WAR 中的实际 jar、类文件引用和服务器启动异常。容器实现、Jakarta 规范与第三方库有版本对齐问题,不能随意把 Java EE 8 实现 jar 与 Jakarta EE 11 API jar 同时打包。SQL 驱动与 javax.sql 单独检查,避免为了移除 javax 而破坏受管 DataSource。

第二层是 HTTP/JSON 契约:旧系统 POST /api/requests 的正常创建、错误 JSON、状态码、请求 ID、会话 Cookie 策略与 CSRF 检查,在新 WAR 上用相同输入复跑;URL 不应因新容器的上下文根或默认 servlet 路径变化悄悄漂移。第三层是业务行集合:相同合成 SKU 与金额产生同一租户、状态、版本和订单唯一约束;恶意客户端伪造租户字段仍应被拒。曾经在旧系统中依赖容器登录角色的 @RolesAllowed,迁移后要检查新身份存储、认证机制、角色映射与对象权限。401 变为 200 不是“配置差一点”,而是严重的安全回归。

第四层是受管事务与错误分支。用独立 PostgreSQL 16 副本或旧环境兼容的数据库配对,而不是让新旧 WAR 同时写同一张真实订单;对创建申请后人为触发异常,核对明细和申请是否一起回滚。错误注入、回滚策略和身份测试必须保证旧版有实测基线,不用新版本的理论行为回填旧版。不同版本容器的日志文字、线程顺序或异常包装类可以不同,但约定的 HTTP 响应与数据库不变量不能任意改变。外部邮件/消息的依赖属于另一层,需要分别冻结 broker 和接口版本;不能因为基础健康探针可用就跳过消息/权限矩阵。

最小迁移实验与反例

迁移成功的失败实验不止“故意保留一个旧 import”。还应比较输入校验顺序:旧环境拒绝数量为零的明细,新环境若解析 JSON 时变成默认值并写入数据库,HTTP 错误码相同也不能算等价。让供应商名称包含特殊字符、金额在允许精度边界、租户缺失或 null,在两个版本比对 DTO 校验、服务器核价和 JDBC 参数绑定。参数错误应在外部通知/outbox 写入前终止,任何非法请求在数据库不留下新申请、明细或通知任务。对外泄露的异常栈应按安全策略清理,但内部日志仍留足以定位版本与资源配置问题的关联 ID。

负向链还要检查回滚后能否恢复:在新容器故意把 JNDI 数据源名绑定到不存在的资源,先记录部署期还是首次借连接失败,再恢复正确资源,用原输入重试并确认只有一次有效订单。如果只检查新 WAR 在错误资源下返回 500,没有恢复后数据库行,就不能证明迁移过程完整。角色映射错误必须使用两个不同合成主体与跨租户 ID 复现,确保修复后未经授权主体不能读/改另一个租户;不能用单个管理员主体测全矩阵。所有新旧运行记录分别保存构建哈希和时间,才能避免把两次不同代码快照混成“同版本对照”。

阶段一在隔离旧运行时运行原始采购链:合法草稿→提交→审批→订单,记录请求/响应、两类租户的数据库行、角色拒绝与人为回滚;同时记录旧源码 SHA、JDK、服务器和 WAR 哈希。阶段二在新分支按模块迁移 API 与 XML,按目标 Jakarta EE 11 依赖重新构建,部署到 Open Liberty 26.0.0.5,再重放同一份合成数据和失败路径,比较状态/金额/版本、错误类别和权限矩阵。独立旧系统若没有某功能,不应在新系统伪造“旧版一致”,只能把功能变更列出来。旧应用编译目标 JDK、目标 JDK 21 的编译语法和字节码等级需分别检查,避免在旧服务器上加载新字节码后把版本错误误判为 API 迁移失败。

有价值的负向对照至少两项:故意保留一个旧 javax.servlet API 引用,观察新服务器加载/部署的确切失败阶段并在修复后复跑;故意不迁移一个旧角色约束,检查越权访问未被放行,随后恢复安全配置。只有第一项通过不能证明后一项无漏洞;应用若在启动时报错,不得假造后续 HTTP 403。配置注入只在隔离副本进行,保护原始旧环境,日志中的凭据与身份证明必须脱敏。迁移不同阶段存放独立证据,避免用同一个 HTTP 200 隐含地覆盖构建、资源、权限和事务四层。

当前仓库没有冻结的 Java EE 8 旧工程,也没有旧 WAR 与旧服务器的可检查运行输出;**原始构建、旧版正常/失败、迁移编译、目标部署及回归全部 NOT_RUN。**现有 scenarios/00-health.sh 只验证现有 Jakarta 工程的路由,不是旧版参照;即使将来通过也不能跳过旧环境基线。本篇限定 Java EE 8→Jakarta EE 11 的企业应用边界,不把特殊历史 SOAP 栈缺失自动归结为单纯改包名。

兼容性验证可以拆成四个明确闸口:旧环境能复现原本契约;新 WAR 不带错误 API jar 并通过编译/部署;新容器能访问预期 JNDI 与数据库;同一数据集的正常和失败行为通过。闸口逐一失败时,记录失败阶段并停止将下游结果标为 PASS。迁移期间可能需要并行运行旧新系统,但并行写同一订单表会造成幂等键竞争和难以还原的版本差异;应隔离写入或设计受控流量切换与回退点。回退不是把新 WAR 换回旧 WAR 那么简单:如果已对 schema 做不可逆删除、旧 API 读不了新字段,旧版部署正常也不等于能处理新产生的订单。保留完整旧版二进制、数据库兼容版本及回退演练结果,才能谈发布窗口。

如果旧环境依赖厂商特有 SOAP 或安全配置,迁移时应在依赖清单里单列“平台规范外扩展”,注明目标是否需要独立实现与合同测试;不能把这些 jar 连同旧 javax 包搬入新 WAR 后寄希望于兼容层处理。遗留 Servlet Filter 的顺序可能决定认证、请求 ID 和事务边界的实际观察位置;迁移后用请求 ID 贯穿过滤器、REST 资源、DAO 和审计日志,若匿名请求已经进入采购用例,即便最后映射成 401,仍须检查是否产生副作用。执行到这一步才可以按章报告具体可移植路径,其余未测项保留 NOT_RUN。

版本冻结需要包含测试客户端本身。旧版接口若以特定序列化器把金额写成字符串,新版改成 JSON 数字,数据库值即使相同,外部调用者仍可能解析失败;因此保存原始请求与响应的字节或结构化契约,再对目标系统回放,不只检查 Java DTO 相等。失败响应也须比较是否泄漏内部路径、用户名或堆栈。旧版无法提供的证据要标“无原始基线”,不能按新系统表现替旧系统推定历史结果。

两道带答案的练习

练习一: 批量把旧工程中全部 javax. 改为 jakarta.,结果 jakarta.sql.DataSource 不存在、健康端点又返回 404。应如何分别诊断?

解: javax.sql 是 Java SE JDBC,恢复该包名,逐一核对其他 javax.* 的规范归属;404 需检查 WAR 上下文根、REST/Servlet 映射、部署报错与目标配置,不应由“改回 JDBC 包名”推断 Web 路由也自动恢复。先用独立旧工程的真实响应作为预期,再在目标容器逐层重放。两条问题在此处均未实际复现。

练习二: 新 WAR 的合法下单与旧 WAR 的合法下单都返回 200,但新 WAR 允许匿名批准,旧 WAR 拒绝。能判定迁移已成功只差一个“非功能性配置”吗?

解: 不能。认证、方法权限、对象归属和租户隔离是采购业务契约,匿名放行属于严重回归。检查新容器实际安全机制、角色映射、方法入口和是否误开教学端点,负向用例与数据库行都通过后再判迁移;旧版/新版系统目前均未运行。

官方版本资料与迁移界限

历史入口:Jakarta 官方 javax→jakarta 说明、Jakarta EE 9 Platform、Jakarta EE 11 Platform;Java SE 21 javax.sql 文档 解释保留包名的事实。目标示例选 JDK 21/Open Liberty 26.0.0.5;旧运行时及 JDK 的具体补丁尚未冻结,本文不声称 EE 8 WAR 能部署到目标容器,也不以文字对照代替旧/新双环境验收。