深入 Play 30:生产打包与部署,产物、就绪和停止的边界
一次部署至少要回答三个问题:运行的字节是否来自审核过的构建,实例当前能否接收业务,以及旧实例退出时已经接受的工作到了什么状态。开发服务器返回 200,只回答了某个请求在开发环境中能够完成。 Play 的 production 启动脚本把构建与服务运行分开,但这种分离不会自动产生正确的就绪检查,也不会把强制结束变成有序停止。实验用同一个 dist 产物启动独立进程,经本地代理读取健康、就绪和流响应;随后分别发送 SIGTERM 和 SIGKILL,以客户端结果、新数据库连接和资源日志交叉核对。 产物身份先于启动成功 本篇增量已接入第00篇的累计源码包。共享工程显式启用数据库后55项JUnit全部执行,stage与dist通过;生产重放的16个场景再次通过,见 evidence/batch30-35/shared-build.json 和 shared-production-http。下文隔离构建与测量记录仍保留原始时间,不替换成共享重跑数字。 实验固定 Play 3.0.6、Scala 2.13.15、sbt 1.10.7、JDK 21.0.11、Pekko 1.0.3、Pekk...
深入 Play 29:同一用例在单元、路由与真实 HTTP 中证明什么
金额 1、100 被接受,0、101 被拒绝。这组输入分别通过纯 Java 策略、Helpers.route 和独立进程 HTTP。三个层次都有价值,但相同的结果并不让它们拥有相同覆盖:纯函数没有解析 JSON,路由测试没有经过真实 TCP,网络测试也没有自动变成数据库事务测试。 分层验收需要把每条成功结论限定在它真正经过的边界。一次测试通过,只能覆盖已经执行并断言的行为。 从纯业务规则开始固定输入 ObjectPolicy.amountAllowed 是有限业务规则:amount 大于零且不超过 100。它不依赖请求、Cookie、线程或数据库。纯单元层使用相同四个输入,直接断言 boolean,失败时可以先定位规则或期望,而不必同时排查路由、Content-Type 和服务器启动。 HTTP Controller 还要处理 JSON 形状:字段存在、是整数、能转换为 int,之后才调用金额规则。业务函数能够接受 int,并不证明任意 JSON 数值都能安全进入这个范围。因此应用路由层和 HTTP 层需要补充非法 JSON、错误类型和缺字段等边界;本批实际执行的 parser...
深入 Play 28:请求结果、流终止与业务结果
一次请求的响应期限设为 50 ms,最终返回 504;响应流随后终止,安排在 500 ms 的合成后台完成回调仍然执行。另一次请求先得到 200,客户端读取 16 KiB 后关闭真实 TCP 连接;服务端只生产了 3 块、共 49152 字节,流的完成回调却没有异常。 这两个结果要求指标说明观察位置。Result 可用、响应流终止、后台工作完成、客户端收全,都不能用同一个“请求成功”计数代替。 每个事件对应哪一步 Play 3.0.6 的固定源码提交为 2e56aff7d4e7a74af61e4bd39ec9e3ed7f300cd6。Java Filter.apply 接收一个从 RequestHeader 到 CompletionStage 的函数。这个 Stage 提供 Result;Result 内部的 body 还可以包含尚未完全运行的 Source。 实验的 ObservationFilter 在调用 next 前记录 request_started,在 Stage 产生 Result 后记录 result_ready,再包装 body 的字节流,用 watchTer...
深入 Play 27:CSRF、CORS、Host 与代理信任
相同的 Cookie 表单请求,缺少 CSRF token 时返回 403;加入被 CORS 信任的 Origin 后,在默认配置中返回 200。显式设置 bypassCorsTrustedOrigins=false,同样的可信 Origin、同样缺失的 token 又得到 403。 这组三进程对照中的差异来自框架配置联动。允许跨域读取、校验请求是否携带防伪令牌、判断用户是否有权修改对象,各有独立条件;不能从一个 200 推断这些条件都执行过。 先固定过滤器实际使用的配置 实验固定 Play 3.0.6,源码提交 2e56aff7d4e7a74af61e4bd39ec9e3ed7f300cd6,JDK 21。累计工程早期的合成请求曾使用 X-Play-Lab 作为 CSRF bypass header;本篇启动独立 conf/governance.conf,不 include 早期 application.conf,实际负测携带这个头仍返回 403。 专项过滤链显式包含观测、CORS、CSRF、安全响应头、AllowedHosts。CORS 位于 CSRF 之前,使其可信来源判...
深入 Play 26:签名身份、对象策略与订阅撤销
alice 的签名 session 可以访问对象 1,但访问对象 2 得到 403。bob 的权限恰好相反。alice 订阅对象 1 的 SSE 后撤销权限,下一次事件检查使连接中断;bob 的 WebSocket 在撤销后的下一条消息上失败,客户端却收到 Close 1000。 HTTP 身份、对象访问权、连接终止和业务成功分别有自己的证据。Close 1000 不能覆盖服务端已经记录的授权失败;签名有效也不能替代一次对象级检查。 从 Cookie 字节到请求身份 Play 3.0.6 的固定源码提交为 2e56aff7d4e7a74af61e4bd39ec9e3ed7f300cd6。Java 的 Security.AuthenticatedAction 与默认 Authenticator 先调用 getUsername;有值时向 request attribute 写入 USERNAME 后继续执行,没有值时返回默认 401。默认 getUsername 只读 req.session().get("username")。 这段 Action 没有解析密码...
深入 Play 25:生命周期与后台任务,停止应用时还剩什么
测试进程收到 SIGTERM 时,PostgreSQL 中仍有一个 PgSleep 查询。应用先拒绝新任务,等待已经接受的任务提交,再关闭执行器、连接池和文件。两个故意失败的 stop hook 被记录下来,后面的清理仍然执行。 退出码 143 只说明进程因 SIGTERM 结束。任务是否提交、线程是否终止、文件和池是否关闭,需要各自的终态证据。本实验同时保留生命周期日志和停止后由新连接读取的数据库结果。 自建资源需要明确的持有者 app/lifecycle/ManagedDatabaseJob.java 是单例的有限实验任务持有者。它按需创建一个 scheduler、一个 worker、一个最多两个连接的数据库池和一个 FileChannel,每次只允许一个任务在途。 scheduler 负责安排任务开始,worker 执行真正的同步 JDBC。两者分开后,任务执行不会占住定时器线程,但这个结构仍然需要容量、拒绝和关闭规则;创建两个执行器本身不提供可靠任务系统。 flowchart LR Start[接受提交] --> Scheduler[有限调度器] ...
深入 Play 24:Evolutions 与模式升级,应用和数据库怎样协同
一段升级脚本先创建表,再向不存在的表插入。自动提交开启时,第一张表留在数据库中,play_evolutions 留下一条 applying_up 记录;自动提交关闭时,这次实验的表创建和版本记录都回滚了。 应用启动失败只是共同的表面结果。下一步应先检查实际 schema 和迁移元数据,确定哪些语句已生效,再决定修复方案。把版本记录直接改成成功,无法补上缺失的数据或撤销已经执行的 DDL。 脚本版本、元数据和数据库事务 Evolutions 按版本组织 up/down 脚本,比较待应用脚本与数据库中的迁移记录,再执行差异。Play 的机制负责版本与执行流程;DDL 是否能够回滚、语句会取得哪些锁,仍由具体数据库及语句决定。 实验固定 Play 3.0.6,源码 SHA 为 2e56aff7d4e7a74af61e4bd39ec9e3ed7f300cd6。入口是 ApplicationEvolutions.scala 的启动应用逻辑,以及 Evolutions.scala 中的脚本计算和执行。 flowchart LR Files[版本脚本] --> Diff[比较版...
JPA 持久化 00:规范、Provider 与启动的边界
persist 能编译,却不一定能写入一行 采购申请调用 entityManager.persist(request),编译成功只能说明 Java 类型与注解可用;EntityManagerFactory 创建成功,也不等于 PostgreSQL 里已经有申请行。依赖解析、Provider 发现、获取 JDBC 连接、映射校验、事务提交分别可能失败。判断一套 JPA 程序是否启动,首先要说明“启动”指哪一层,再用该层的证据验证。这里用采购审批累计工程中实际存在的入口,而不是搭建另一份互不兼容的演示数据库。 本篇源码、迁移、脚本与测试可从固定版本完整归档取得;版本 1b08ada,SHA-256 见源码清单。 本系列的流程是 DRAFT → SUBMITTED → APPROVED → ORDERED,或 SUBMITTED → REJECTED;只有 APPROVED 可以创建订单。金额由明细计算,一张申请最多一张订单,租户不能跨界访问,重复请求不得重复创建。启动实验只建立第一条申请,并不能凭一条 INSERT 证明所有这些约束。与 Java EE/JDBC 主线共享业务语义,却...
Java EE 企业应用 00:从空目录到最小 WAR,健康响应证明了什么
健康接口返回了 ready,采购系统就能交付了吗 一个开发者拿到采购审批应用,第一件事通常是运行服务。如果浏览器显示 ready,容易把它理解成“系统已经启动”。可是采购申请能否提交、订单是否只生成一次、其他租户能否读取申请,都没有经过这个接口。这里需要先把“从空目录运行”拆成可检验的阶段:命令行能编译源码;服务器能加载单个 WAR;HTTP 能抵达健康资源;业务事务能提交;权限和业务结果能被核对。第 00 章只走到前三项,不用健康响应冒充后面三项的证明。 本篇源码、迁移、脚本与测试可从固定版本完整归档取得;版本 20261005-local(基于 1b08ada),SHA-256 见源码清单。 教学对象是单 WAR 的企业采购系统。实际采购还需要申请明细、审批记录、订单、通知和审计;本章只检验健康接口。当前工程另有未认证的 /api/db-check 诊断入口,且在 JAVAEE_DEMO_MODE=true 时才开放未认证的 /api/lab/requests 教学入口;二者仅供监听回环地址的隔离实验使用,不能作为生产服务对外暴露。先修自测也因此很具体:能解释 HTTP 20...
深入 Play 23:事务与幂等,数据库提交如何关联 HTTP 结果
两个请求都收到 HTTP 504,一个对应的订单尚未提交,另一个对应的订单已经提交。外层响应预算耗尽时,数据库操作可能处于不同阶段;重试若直接生成新订单,就会把“没有收到成功响应”误当成“没有发生写入”。 事务负责一组数据库修改的原子性,幂等键负责把多次请求关联到同一次业务操作。响应超时、任务完成和事务提交需要分别观察,单独检查 Stage 或 HTTP 状态不能替代数据库终态查询。 withTransaction 等待同步 block 返回 Play 3.0.6 的 Databases.scala 把连接设为非自动提交,调用 block,取得返回值后 commit;普通异常路径 rollback,外层 withConnection 再关闭连接。 返回值的泛型没有改变这一时序。若 block 返回 CompletionStage<T>,这里取得的是 Stage 对象,源码不会等待它完成。事务边界包住的是同步调用 block 的过程。 sequenceDiagram participant Worker as 工作线程 participant Tx a...







