深入 Play 32:版本迁移,把编译通过和协议兼容分开验证
把 Play 2.9 应用中的 akka.util.ByteString 留到 Play 3,编译会失败。把 play.server.akka.server-header 留到 Play 3,应用却能启动,配置查询也能返回旧值,实际 HTTP 响应仍然没有那个 Server 头。两处改动都涉及 Akka 到 Pekko 的命名迁移,故障出现的位置并不相同。 版本迁移需要同时检查 Java 类型、解析出的依赖、配置的实际消费者和网络行为。compile 能发现缺失的类,无法证明某个字符串配置已被服务器使用;单元测试能检查 Result,也无法替代打包进程在线路上发送的完整响应。 四个组合,分别改变框架和 Scala 实验使用四个独立的 Play Java 工程。前三个固定 Scala 2.13.15,依次迁移 Play 2.8.22、2.9.6、3.0.6;第四个保持 Play 3.0.6,把 Scala 改为 3.3.4。所有组合使用 sbt 1.10.7 和 Amazon Corretto 21.0.11,同一套 Java 路由和 Python HTTP 断言贯穿整个矩阵。 ...
深入 Play 31:性能与容量,排队位置如何改变观测结果
同样提交 40 个 JDBC 请求,默认 dispatcher 的 40 个请求全部返回 200,专用执行器却有 38 个返回 504。只看状态码,默认模式似乎更好;只看 HTTP 返回吞吐,专用模式又明显更高。数据库最终都提交了 40 行,专用模式中提前返回的超时没有减少 SQL 工作。 这组结果来自相同连接池大小、SQL 和请求数量。差别在于任务在哪里等待,以及计时器从哪里开始计时。容量实验需要同时解释客户端等待、应用排队、数据库完成和资源归还,单个 QPS 数字无法表达这些关系。 固定负载与可复现的观测范围 本篇增量已接入第00篇的累计源码包。共享工程55项JUnit全部执行;容量网络再次跑完三轮48个cell、1440次测量,见 evidence/batch30-35/shared-performance-http。下文数值来自原隔离轮次;重跑受本机并行任务与调度影响,不能将不同轮次百分位混合成一组测量。 累计工程的 lab/performance_checks.py 启动一个真实 production stage,使用回环 HTTP 完整接收响应,再以新数据库连接核对...
深入 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 主线共享业务语义,却...






