换了技术栈,审批结果不能跟着换

采购申请 519 属于租户 A,状态 SUBMITTED,版本 7,总额 4900 元;主体只有 A 的 APPROVER 角色且限额 5000 元。审批后预期状态 APPROVED、版本 8,只形成一次审计事件。另一张跨租户或超额的申请不应因从 Jakarta EE 改用 Spring Boot/MyBatis 而变得可批;若事务回滚,审批和审计必须一起消失。这个例子是技术栈对照的固定输入,而不是本机已执行的受保护采购接口。现有 WAR 暂未接入可信 principal、审批额度和审计表;先修 04、09、12–23、35 的实际实验范围必须如实核对,不用后续章节的题目代替已经完成的代码。

在 Jakarta EE 版本,HTTP 入口由 Servlet/REST 承接,容器创建 CDI 或会话 bean,用受管 DataSource 执行 JdbcRequestStore 的参数化 SQL;受管事务决定提交/回滚,安全上下文提供身份,但业务要继续做对象权限。在另一版本,Spring Boot 建立应用上下文、注入服务与 DataSource,Spring 声明式事务通常通过受管代理进入,MyBatis mapper 构建并执行 SQL,HTTP 与认证由该应用的 Web/Security 配置承接。MyBatis 本身不是安全上下文,也不能由 @Mapper 注解产生跨租户隔离。两边在 SQL、约束、数据集、输入和最终数据库行保持一致的前提下,才有资格比较职责归属,不是测“谁更快”。

固定一个入口和一条 SQL

业务输入应带相同的预期版本与稳定请求标识,不能一边传 version=7、另一边只传申请 ID。若旧版在 DAO 中读取数据库报价,移植版不得把 total 改成客户端提交值,否则“超额审批被阻止”矩阵已不是同一道题。抽出与框架无关的断言:一次成功更新返回新版本 8,另一审批员对版本 7 的竞争更新返回 0 行;两边都不得因框架自动生成 SQL 而省掉租户条件。对每次结果按受信主体、请求、SQL、数据库事务和响应排列证据,才能知道差异发生在哪个环节。

MyBatis 映射接口中带 @Param 的入参也不是身份凭据;mapper 接到字符串 tenantId 时不会知道该字段是不是来自被篡改的 HTTP 请求。安全边界必须落在 Web 层建立认证主体、应用层验证组织/额度,再把可信上下文传给数据层。审批语义包含主体能否操作和 SQL 是否有资格改这张单两个条件,二者缺一不可。框架对照时如果出现“Spring Security 拦住了 HTTP 请求,但定时任务直接调用 mapper 批了同一张单”,应把这个内部入口列入权限缺口,而不是宣称 Web 层已安全。

对照仅移植“审批一张已提交申请”这一用例,不复制整个采购系统,不偷偷把 Jakarta 版的 priceCatalog 改成客户端价格。两版都用同一 PostgreSQL 16 schema(运行时为避免污染,使用结构相同且相互隔离的数据库副本),相同金额单位、角色与额度规则、相同请求状态机和相同 HTTP 错误分类。正确的 SQL 必须在写入时重查关键约束,而不是先在 Java 查完然后更新一张可能已经变更的单据。

1
2
3
4
5
UPDATE purchase_request
SET status='APPROVED', version=version+1
WHERE id=? AND tenant_id=? AND status='SUBMITTED'
AND version=? AND total<=?
RETURNING id, version;

这里的 tenant_id、额度参数来自受信 principal 的组织与权限映射,不来自客户端自报的 X-Lab-Tenant;现存 JdbcRequestStore.transition 校验租户、旧状态和版本,但没有额度/角色条件,不能把现有 SQL 当成此例已经实现。0 行可能是越权、版本竞争、状态不符或金额不合法;两个实现都不能返回“审批成功”。若设计要求在审批同时写审计,应在同一数据库事务完成,审计失败时回滚两项;数据库唯一约束与版本列承担并发冲突后盾。

两种容器装配并不互相翻译

还要冻结数据源的来源和连接预算。Jakarta 一侧通过 @Resource(lookup="jdbc/Procurement") 从服务器获得受管资源,Spring 一侧由自身配置和连接池构造 DataSource;即使两端都叫 DataSource,连接回收、事务登记、健康验证、池满等待与泄漏诊断的配置位置也不同。比较时固定最大连接数、数据库允许连接数和事务隔离级别,记录服务端实际生效的值,不按某框架默认配置猜测。若一侧使用 HikariCP 另一侧使用服务器原生连接池,响应时间差混入连接池行为,不能给框架做纯粹性能排名。

错误映射也需双边统一:主体未认证返回认证挑战,主体存在但额度不足或对象不可见返回事先约定的 403/404,状态版本冲突返回 409 或冻结的业务错误;不能为了让两种框架的异常类名相同而牺牲保密性。同租户两名审批员以相同版本并发更新时,成功者写一条事件,失败者的事务不应提交“已批准”的审计。若服务把记录从数据库读入内存后只凭 if (version == 7) 判断,两个实现都可能通过检查并覆盖结果;写入处 SQL 条件与唯一约束才是两边可验证的一致性后盾。

Jakarta EE 11 的平台规范覆盖 CDI、Jakarta Transactions、Servlet、Enterprise Beans 等多项技术;实际部署仍需为 jdbc/Procurement 配置受管资源,并确定 Web/事务/安全入口采用什么功能。注入一个 DAO 成功不意味着 JDBC 获取的是 XA 连接,也不能推断其事务总是自动加入。Spring Boot 是应用启动、配置和依赖集合的框架,MyBatis 作为 SQL 映射层需要正确的 DataSource 与事务管理器;Spring 的 @Transactional 依赖通过代理/织入的调用路径,普通对象或内部自调用绕开代理时可能没有期望边界。Jakarta CDI 事务拦截及 EJB 事务规则也要按各自契约检查,不能机械地把 Spring 的“未检查异常默认回滚”复制给任意 Jakarta 受管方法和异常类型。

目标 Spring 端的最小入口可以写成 ApprovalService.approve(actor,id,version),通过 Spring 容器实例调用、在 mapper 上使用上面的 SQL 并检查更新行数。下面只是接口契约,不是完整可运行 Spring 工程;AuthenticatedActor 必须从真实认证中建立。

1
2
3
4
5
package blog.javaee.comparison;

public interface ApprovalPort {
void approve(AuthenticatedActor actor, long requestId, long expectedVersion);
}

缺席的 AuthenticatedActor、SQL mapper、事务配置、权限和审计实现都必须单独列出,不能给这个接口加上 @Transactional 就声称双边等价。Jakarta EE 端用原有 ProcurementUseCases 的端口方式调用 JDBC 适配器;移植后 Spring 端可用 MyBatis SqlSession/mapper 明确执行 SQL。SqlSession 的生命周期和事务归属由 MyBatis-Spring 集成配置管理时,应由实际依赖版本的官方资料确认;不能同时手工提交会话又期待 Spring 回滚。两边都在受信调用入口明确记录交易 ID、主体 ID、审批 ID 和最终数据库行,避免只比一张漂亮的 HTTP 回包。

失败矩阵不是“改造后随便跑一次”

事务回滚试验应在申请条件 UPDATE 返回一行后、审计 INSERT 前后分别制造异常:前者用于证明更新未提交,后者用于证明同事务审计失败不会留下审批。两个实现若把审计写进异步队列,实验定义已经改变,不能拿原来“同事务审计”预期比较;若需要异步审计,则应同事务写 outbox 并另外验收送达。对于 @Transactional,要把运行时异常和 SQLException 这类受检异常分别列格,并检查各自明确的回滚配置,不因两个框架注解名字相同就假设完全相同。数据库最终行而不是调用时捕获到的异常文字决定结果。

数据准备必须可重置。两个独立数据库从同一 schema 迁移和同一合成种子恢复,审批员权限、额度与订单版本每次重跑前都固定;如果先跑 Jakarta 正常路径把版本从 7 推到 8,再用相同库跑 Spring 路径,后者自然会返回冲突,并不能证明它的授权或事务实现有问题。可为每次测试生成固定业务键的独立副本并记录 schema 校验值,回放并发交错时控制启动栅栏而不是依赖多次运行碰运气。同库 PostgreSQL 16 的 RETURNING 行数与隔离等级也应在两侧一致;换成不同数据库再比较锁行为会引入方言差异。

装配失败也属于对照范围。Jakarta 资源名写错时,容器可能在部署期或首次借连接时报错;Spring 连接池的数据库地址错误可能在创建 Bean 或执行 mapper 时暴露。应分别记录发生阶段、异常根因和对外安全响应,不能因为两侧失败时机不同就断言业务不等价。更换驱动或库连接参数时先确认 SELECT current_database() 与 schema 版本,避免将另一应用的数据库误当采购库。对照在隔离复制库上进行,不通过修改现有 server.xml 或主线实验源码来“让差异消失”。

安全矩阵必须有角色和对象两层独立负例:有角色但跨租户、有角色同租户但超额、同租户同额度但申请版本旧。Spring Security 可提供主体和方法/HTTP 规则,但受信租户映射、额度、申请所有权仍是业务服务责任;Jakarta Security 亦然。两边都不应让请求中的 X-Lab-Tenant 决定租户归属,否则虽然 HTTP 状态相同,只是两个实现重复了同一个漏洞。审计只记录受信主体 ID 和原因码,避免在框架诊断日志里泄漏原始凭据。

给两个隔离数据库灌入同一组合成数据:正确审批、匿名审批、租户 B 的审批、超额审批、旧版本审批;对每次调用检查状态、版本、订单与审计行,并保存实际 SQL 条件、错误映射及执行入口。再构造审计写入失败:两个事务都必须回滚,若 MyBatis 使用了未参与 Spring 管理的另一个连接,可能出现只提交申请状态;如果 Jakarta DAO 从未加入受管事务,也一样可能出错。正常路径不只断言 HTTP 204,失败路径也不只查异常文字,最终数据库行才是比较对象。以同一申请版本并发竞争时两边只能有一个状态迁移获胜,不能因为异常类型不同而放宽业务约束。

对照报告应把差异拆成装配位置、调用入口、事务管理者、连接归还与部署物:WAR 到兼容运行时和独立 Spring Boot 进程的启动/配置不同,运行平台占用不可直接归因于“语言框架”。如果要做性能实验,必须另建负载、环境、预热和测量方案;此处没有性能数字,不给产品排名。不同 Jakarta 平台规范版本和 Spring 依赖树可能包含不同 Jakarta API 版本,即使 Java 包名同为 jakarta.* 也不保证可以互换依赖坐标。对比中禁止让受保护的一侧使用真实身份、另一侧继续用可伪造 header,否则测到的差异只是安全要求被删除。

现在仓库没有 Spring Boot/MyBatis 对照工程,没有受认证审批和审计基础表,更没有同数据集两侧的 HTTP/SQL 证据。**装配、正常/匿名/跨租户/超额/旧版本/回滚/并发对照全部 NOT_RUN。**报告预期结果不能被写成实测结论;仅阅读两套文档或在主线 WAR 上执行 clean verify 也不是迁移后的可运行验证。

验证矩阵建议按“用例 × 两个实现”逐格保留输入哈希、数据库版本、最终行集合及测试执行标识,而非靠两个程序同时运行的截图。对 SQLException 等受检异常,Jakarta Transactions 的 rollbackOn 配置与 Spring 的回滚配置分别检查;两者默认行为不能无条件假设相同。部署结束后重启服务、重放相同幂等请求,检查数据库唯一约束仍然生效;若只在内存中记录已处理键,一重启便失去业务等价。不要根据单个正常请求成功推导“后续消息/发布阶段两边都一样”,本章只移植一个审批用例。

部署方面要核对工件和服务真正运行在何处:Jakarta 的单 WAR 依赖目标 Platform 容器,Spring Boot 的可执行工件通常带内嵌 Web 服务器与其自己的依赖管理;两者的健康端点可能都返回 200,却可能一个查询的是环境一、另一个读到环境二的库。使用受控 SQL 读取 current_database() 并核对合成申请 ID,再执行批准/回滚矩阵。资源停止后的连接借用、线程清理和审计异常分类分别收集,不拿一方“能启动”推导双方已在同等部署条件下可替换。仅移植一个用例还不能宣称已迁移通知、批处理或全部 Jakarta 平台能力。

两道带答案的练习

练习一: Spring 版从 Controller 内部 this.approve() 调用了标注事务的方法,MyBatis 更新申请后审计写失败,数据库保留批准状态。把业务规则改为“审计可丢”是合理对照吗?

解: 不合理,比较前已经约定审批与审计同事务。应确认调用是否经过 Spring 代理、mapper 是否使用参与同一事务的 DataSource、异常类型是否触发回滚,然后重跑失败数据集并核对两个表。不能用 Jakarta 侧的成功用例或 Spring 侧 HTTP 500 替代回滚证据。

练习二: 把旧版 JDBC SQL 换成 MyBatis 自动映射后,跨租户申请仍被批准。是 MyBatis 数据库方言问题吗?

解: 先检查 SQL 是否保留 id + tenant_id + version + status + total 条件,租户是否出自认证主体而非 HTTP header,并核对 mapper 更新行数和越权数据库最终状态。ORM/mapper 不会自动施加业务权限;若变更丢掉租户谓词,是功能回归而非产品差异。实际回归测试尚未执行。

版本化资料与结论限度

Jakarta 一侧为 JDK 21/Jakarta EE 11/Open Liberty 26.0.0.5,SQL 为 PostgreSQL 16 显式 SQL,规范参考 Jakarta EE 11 Platform 与 Jakarta Transactions 2.0;框架参考 Spring Boot 3.5 参考文档 与 MyBatis 3 官方文档。Spring Boot 3.5 与 MyBatis 的具体补丁/集成版本尚未冻结,不能把这些文档引用冒充依赖锁文件或可执行移植报告。MyBatis 没有替 JDBC 主线做真实安全验收。