Java EE 企业应用 39:一个企业应用怎样完成交付验收
交付结论必须能指回业务终态
采购人提交两支纸和三支笔,审批人批准后建单,重复请求只取得同一订单;同时数据库写入失败、通知未送达、租户冒用与发布回切都不能制造第二张订单。这样的交付目标不是一次 /health 的 200,也不是一张“所有测试通过”的截图。它要求每条需求都能对应入口、输入、失败注入、独立可读的终态和原始记录;未实现的能力应留空,不能靠旁边的绿色用例补齐。
教学基线在 examples/javaee-enterprise/README.md:JDK 21、Jakarta EE 11 Platform、Open Liberty 26.0.0.5、PostgreSQL 16.15、pgJDBC 42.7.7。工程已有隔离 WAR、CDI/受管事务、JDBC 与 SQL、教学采购接口和部分原始验证记录。JAVAEE_DEMO_MODE=true 通过不可信 X-Lab-Tenant 开放本机实验入口,不提供正式认证;尚无真实 broker、通知消费者、旧库迁移和滚动回退实验。这是可运行的验收底座,不是完成全部需求后的生产交付。
当前代码能解释一些现成断言从哪里来:LabRequestsResource 在 /procurement/api/lab/requests 收入 SKU 与数量,服务端 FixedPriceCatalog 计算两支 paper 与三支 pen 的合计 13.75。RequestState 限定从草稿到提交、批准或拒绝,再由批准进入已下单;直接从草稿建单会抛业务冲突,映射成 409。JdbcRequestStore.transition() 用旧状态与版本作 SQL 条件,订单表以 UNIQUE(request_id) 守住同一申请的持久单数。这里没有身份令牌验证,也没有 outbox 表或 broker 消费路径。交付表应依据这份代码的实际边界填写,而不是依据系列计划中将来要做的功能填写。
用可复查的行集合决定是否通过
需求追溯记录至少要给出:业务不变量、实际入口、权限主体、正常与失败输入、HTTP/消息结果、独立连接读到的申请与订单终态、软件版本/部署物摘要和证据文件位置。对于金额,服务端按价格目录计算 13.75,不能信任客户端传入的金额;对于“每申请至多一单”,应用的顺序重试要配合数据库 UNIQUE(request_id),并在并发冲突后用新事务读回。若客户端断连,缺失响应不等于未提交。
字段也有不同的证据强度。09-lab-procurement.sh 的退出零说明它按脚本定义的顺序完成了 201 创建、409 提前建单、404 另一演示租户、204 提交与批准、200 下单和重试,并读回一次 ORDERED|13.75|1。其中 404 是对调用方自己给出的另一租户字符串做了 SQL 谓词核验,不证明“当前人被认证为该租户”。重复 200 的同一订单 ID 是顺序重试,不证明两客户端同时到达时都可以无异常得到同一 HTTP 响应。若对外承诺并发幂等,必须额外控制独立事务交错,并对失败响应和最终库中唯一订单分别记录。
18-lab-rollback.sh 调用 /abort,ProcurementUseCases.draftThenAbortForLab() 在受管方法里调用写入后抛 IllegalStateException。脚本检查注入 HTTP 500,另一 PostgreSQL 连接按失败租户查询零行,同时比较两个主键序列的前后变化。序列上涨表明发号发生过却不等于业务记录提交;事务中 SQL 发生与事务提交是两个时点。这种对照比“收到 500 就认为安全回滚”更扎实,但也只覆盖同一库、该异常点,不能代替两个资源的 XA 恢复或业务出站请求失败。
| 要求 | 现在可核对的证据 | 放行前缺口 |
|---|---|---|
| 构建、健康与受管数据库 | Maven 构建,00-health.sh、05-datasource.sh |
对应本次部署 WAR 与配置的摘要、冷启动记录 |
| 正常采购、顺序重试、数据库订单数 | 09-lab-procurement.sh 对随机实验租户做状态/金额/订单断言 |
正式身份、跨请求并发和全部入口的租户边界 |
| JDBC 写入后故障回滚 | 18-lab-rollback.sh 注入 500、独立连接查零行 |
提交结果不确定、数据库中断与恢复后的业务读回 |
| 异步通知完成或失败重投 | 无 broker 和消费者运行证据 | 消息 ID、去重账本、重投/失败归档及送达边界 |
| 更新与回切后仍能读写旧申请 | 只有空库初始化 SQL 和单份 WAR | 双版本兼容窗口、旧库副本与真实回切演练 |
表中的当前证据有归档的历史运行,但没有在写本文时重新执行;旧记录只能证明其记录的工程版本和配置。状态是“部分可核对,整体 NOT_RUN”,不能把“写出验收步骤”标记成 39 的 PASS。消息确认只能表明 broker/消费者在相应协议点做了什么,无法从 HTTP 200 单独推断采购人收到通知。
用户需求与证据的关系还要考虑反证。若验收目标是“任何租户不能访问别人的订单”,就要分别测试创建、提交、批准、建单、按 ID 读取、列表查询等真实入口;当前工程缺正式身份及列表/查询 HTTP 入口,因此即使现有跨演示头提交是 404,也不能给整张租户权限矩阵盖章。若目标是“拒批永不建单”,需要为拒批的申请再触发下单,并从数据库核对订单数与状态;不能只拿审批前建单的 409 顶替。若目标是“通知只发一次”,需要可识别的消息 ID、重投/去重账本和外部系统收讫证据,现有 WAR 不具备这条链。
在隔离实例重放已具备的部分
先按工程 README 创建 javaee_lab 专用库、应用 db/migrations/001-initial.sql、构建并部署 WAR,配置只监听 127.0.0.1 的容器;启动服务器进程前须设置 JAVAEE_DEMO_MODE=true,客户端后设不生效。下方命令从仓库根目录进入工程,在已经运行的同一隔离实例上做当前可检查的部分:
1 | |
业务脚本从“提前建单应失败”走到审批建单,并核对 ORDERED|13.75|1。此处的 404 仅拒绝与演示头不同的租户字符串,不等价于认证用户的对象授权。记录每条命令的退出码、原始输出、数据库版本、服务器配置和实际部署 WAR 的 SHA-256,不能仅凭构建目录文件哈希推断运行包一致。
对同一隔离实例可加一个独立数据库核验,避免仅复述脚本打印的字符串。此命令会额外跑一次正常场景并从 stdout 取本轮生成的租户和申请 ID;用 psql -v 做安全的字符串引用,失败时应保存原始输出与退出码:
1 | |
预计结果是 ORDERED、版本 3、金额与明细合计都是 13.75、订单数为 1,版本值根据源码三次条件更新推导,本文没有对新增的 SQL 命令实跑。若没有结果行,需要先核对请求是否真正提交和脚本解析的申请 ID 是否相同;若计数超过 1,既要看当前库是否具备 UNIQUE(request_id),也要检查查到的是否是另一个数据库。单条 SQL 的返回不能证明所有其他租户申请均安全,仍需用负向请求与全量业务键核对。
有界的故障重放使用单库回滚场景,不借此冒充 broker 中断:
1 | |
预期 HTTP 500,脚本退出 0,随机失败租户在数据库中的已提交申请数为零;失败后再次运行正常业务脚本,应检查新租户仍能建单。过去一次有源文件哈希与原始退出码的记录位于 writing-plans/javaee-enterprise/verification/20261004T063400Z-pg16-jta-rollback/RUN.md,并非此轮重跑。DB 断连、broker 重投、审批者越权和回切仍缺入口/实现或控制变量;不得以注入抛错替代。补齐条件和预期终态见 交付验收合同。
还可以对库中整体参照关系作只读审计:如果失败事务留下了没有对应申请的明细或订单,外键应该拒绝这类已提交状态;查询零行是进一步核验,但不等于验证了消息或外部通知没有发出。
1 | |
这条 SQL 扫描整个隔离库,显示非零应先核对约束是否被移除或是否连错库;显示零也只说明参照完整性,不能保证每一张订单和申请租户相等。恢复动作要按故障类别选:提交前异常读回无行,可以再按业务协议决定是否重新创建;提交回执丢失则先按旧申请 ID 找订单,不得直接创造新申请;容器部署失败先确认哪个版本仍接流量,再重放该版本允许的路径。
对于消息与发布,验收门槛应当明确写“无入口/无证据”。将来若接入消息,先记录发送事务与 outbox 行,再令消费者处理同一消息两次,要求业务去重账本只产生一次有效副作用,并区分 broker 确认与外部通知收讫;目前代码没有 outbox 表,这些条件没有任何一次通过记录。发布回切也应在旧库副本上跑新旧 WAR 的读写矩阵,同时保存配置版本和回切后的申请终态;仅凭本篇脚本可重放的单实例顺序采购,无法签署这些项目。
一次正式验收可以把正常与故障记录按申请 ID 串成一条可重放的证据链。先记录由测试租户提交的 SKU 与数量,检查服务端金额;再由被认证的审批者完成状态迁移,保存旧/新版本与下单响应;在同一申请上重复请求并读回订单数量;最后从另一个身份尝试读写该申请,确认拒绝且数据库终态没有改变。当前身份接口不存在,不能用演示头填充“被认证的审批者”栏。业务方若要求采购人最终收到通知,还需将订单 ID 与发送、重投、外部收讫记录一一相连;如果只有消费成功记录,送达结论仍为未知。
失败账本也需有恢复判据。数据库不可用时记录错误发生在取连接、写 SQL、还是提交确认之后;前两类与最后一类的重试策略不同。对于发生在提交确认之后的未知结果,不要再次无条件创建申请,而要带已有业务键读回;若接口根本没提供可重用键,验收应提出协议缺口而非依赖请求 ID 猜状态。等数据库恢复,先验证可读取已有订单及原有申请,再验证能处理新租户的新请求,并逐一核对失败窗口的业务 ID。当前脚本只注入提交前异常,未提供数据库断连编排,也没有这张恢复账本。
实施者应将“可做但未跑”和“工程尚无入口”分成两种未完成项:新库上的只读 SQL、现有业务脚本属于前者,仍需绑定当前构建摘要和本次退出码;正式身份授权、消息重投与滚动回切属于后者,在功能出现前不能给出有效命令。只有每条放行要求都有实际输出、独立终态和责任边界时,整体结论才可能由 NOT_RUN 改为可验收。把局部脚本的零退出误当成全部采购流程已交付,会让最关键的故障恢复与访问控制永远没有负例证据。
记录还要注明验收当时的请求源、服务器进程启动环境和数据库名称。JAVAEE_DEMO_MODE=true 如果只在测试终端而不在容器进程内,教学接口可能返回 404;它与按租户 SQL 查无数据的 404 外观相同,根因却完全不同。区分这两类失败要保存启动配置与服务器日志,再用 05-datasource.sh 确认目标库。即便所有实验结果符合预期,正式环境也必须关闭这个不鉴权的教学入口,另建受管身份的可验收路径。
交付报告的末尾应列出阻断项及负责人,而不是把缺失项从矩阵中删掉。某个脚本退出零只能签署其对应的一格;当身份、消息和回切仍缺实现时,整体结论维持 NOT_RUN,用户也能据此判断下一次演练应先补哪个入口。
两道练习
练习一: 发布记录有 Maven 成功、/health 200 和创建订单 200,却没有数据库查询结果。可以签署“重复请求至多一单”吗?
解: 不能。核对同一申请两次请求返回的订单 ID,再由独立连接按 request_id 统计数据库订单,失败和响应丢失时也要重新读回。若要求并发,额外以两个独立事务控制交错并核对唯一键和最终行集合;顺序脚本并不覆盖这些交错。
练习二: 通知消费者报告成功处理,用户没收到通知。应把这条需求写成 PASS 还是 FAIL?
解: 先定义“通知完成”的协议点:队列确认、外部通知服务接收,还是最终送达,各需不同证据。若需求是用户收到,仅有消费者成功不够,不能标 PASS;现有工程连消费者的实际运行记录也没有,只能标 NOT_RUN,补充消息 ID、去重记录和外部送达回执后才能判定。
放行边界与官方出处
此次可重放的只是一台本机容器、单库 JDBC、未认证的教学路径和受控回滚。生产放行还需要正式身份与权限矩阵、审计敏感信息策略、完整消息与恢复链路、容量基线和可回切部署。没有二进制/配置/数据版本的对应证据,就不应承诺“可恢复”“零丢失”或“跨容器可移植”。
- Jakarta EE 11 Platform 规范:平台提供组件/资源的范围,不保证采购业务验收。
- Jakarta Transactions 2.0 规范:本地受管事务的语义参照;不能由此推出消息 exactly-once。
- PostgreSQL 16 Constraints、PostgreSQL 16 Transaction Isolation:唯一约束与多事务核验的数据库依据。
- Open Liberty 26.0.0.5 Jakarta EE 11 feature:本地部署的实现版本,不代替另一个服务器的实测。






