深入 Play E06:OpenTelemetry,从上下文到真实接收证据
请求返回200,Collector可能一条span也没有收到;Collector返回200,也不代表订单已经提交。遥测链路和业务链路拥有不同的队列、超时与终态,接入OpenTelemetry需要分别验证这两条路径。 在Play中,Java Action、独立执行器、Scala Future、WS回调和Pekko流会改变代码执行的位置。一个traceId出现在几行日志里,只能说明字符串相同。父子关系还需要检查每个span的parentSpanId,传播需要查看真实下游请求头,导出需要看到真实接收器解析出的span。 固定组合与实际生效的插桩 实验工程位于 play-electives/e06-otel,不修改系列累计应用。生产模式只加载Java agent,并使用OpenTelemetry API补充业务阶段;SDK构造仅出现在独立JUnit进程中。 组件 冻结值及核验方式 Play / Scala / sbt 3.0.6 / 2.13.15 / 1.10.7,实际stage包 Java Corretto21.0.11,Java21 Java agent 2...
深入 Play E05:Pekko Actor 的邮箱、状态与生命周期
一次 30 ms 的 ask 超时后,HTTP 客户端收到 504;300 ms 的 Actor 工作仍然完成,迟到的回复进入 dead letters。另一组实验向容量为 4 的邮箱发送 20 条业务消息,只有 4 条执行,16 条被丢弃。把状态放进 Actor,可以限制谁修改它,却不会自动获得取消、背压或持久化。 在 Play 中,控制器交付响应、ask 收到回复、Actor 结束工作与应用完成停机是不同的事件。诊断记录需要用同一个消息 ID 将它们关联起来。 版本与独立实验 工程 play-electives/e05-typed-actors 固定 Play 3.0.6、Pekko Actor Typed 1.0.3、Scala 2.13.15、sbt 1.10.7 与 JDK 21。Play 源码提交为 2e56aff7d4e7a74af61e4bd39ec9e3ed7f300cd6,Pekko 为 4f77c8108aaf548a65531d2c8807da13dbba8146。实际 stage 中的 JAR 名称与摘要由 stage-manifest.json 保存...
深入 Play E04:Pekko HTTP 与 Netty,相同 Controller 的传输边界
两份字节相同的 Controller、路由和配置,都能正确解析 JSON、传输数据和完成 WebSocket 回声。向正在处理请求的生产进程发送 SIGTERM 后,结果发生了分叉:Pekko HTTP 返回完整的 200 completed,Netty 客户端收到 RemoteDisconnected。两边的业务回调和 stop hook 却都执行了。 Play 的应用层抽象使多数业务代码可以跨后端复用,连接关闭、协议错误和终止阶段仍由具体传输实现参与决定。本章固定版本,用相同输入、真实 TCP 连接和进程信号比较这些边界。 固定构件,确认真正运行的后端 下载两套服务器后端工程与实验记录,核对SHA256SUMS。解压后的 play-electives/e04-backends 包含Pekko与Netty应用;接入后重跑4项JUnit、两套stage与36项网络观察,20个应用class与生产jar字节一致,见 evidence/e04/integration。WebSocket协议错误与在途停机差异均保留。 实验固定 Play 3.0.6、Scala 2.13.15、sbt...
深入 Play E03:Ebean 与 Hibernate 集成,事务何时结束
一次数据库操作返回失败的 CompletionStage,并不说明它写入的数据已经回滚。两个 Play 实验应用都先写入一行,再返回尚未完成的 Stage。Ebean 已经提交第一行,异步线程写入第二行时又单独提交一次。Hibernate 同样已经提交第一行,异步线程却因为访问关闭的 EntityManager 而失败。 这两个结果由同一个时间边界引起:事务包装器看见的是回调函数返回,Stage 的最终完成发生在包装器返回之后。接入 ORM 时,依赖解析、实体增强、数据库事务和异步任务各有自己的完成条件。compile 成功、SQL 已发送、Stage 失败,分别只能回答其中一部分问题。 两个独立应用,先固定实际运行版本 本篇使用两个独立的 Java 应用:play-electives/e03-ebean 和 play-electives/e03-hibernate。它们共享实验方法,各自持有数据库 schema,不把两个 ORM 同时装进主线最小应用。主线工程入口仍在 Play 00:最小应用。 层次 Ebean 应用 Hibernate 应用 核验方式 构建工...
深入 Play E02:编译期依赖注入,显式组件图能检查什么
把 HelloController 的 Message 参数漏掉,编译器能够立即指出构造器参数不匹配。把运行时配置 lab.greeting 改成空字符串,同一份已编译产物仍会在启动时失败。编译期依赖注入将一部分装配关系变成 Java 表达式的类型约束,配置值、资源状态和业务协议仍需要运行验证。 Play 提供的组件接口允许应用自行构造 Controller、Router、过滤器和生命周期依赖。本章用没有 Guice 运行时构件的独立工程验证这种装配,再以相同业务源码的 Guice 工程作有限对照。是否使用运行时容器与是否经过完整生产启动,是两个可以分别检查的问题。 从加载入口到组件图 下载两套依赖注入工程与实验记录,核对SHA256SUMS。解压后的 play-electives/e02-compile-di 包含manual与Guice应用;接入后重跑2+1项JUnit、两套stage及10个生产/失败场景,见 evidence/e02/shared/delivery.json。启动耗时保留为本机观测,不能推导框架普遍快慢。 实验固定 Play 3.0.6、Scala 2....
深入 Play E01:Scala API 对照,Action、Future 与类型化请求
Java 方法返回 CompletionStage<play.mvc.Result>,Scala 方法返回 Action[A],其中异步处理函数产生 Future[play.api.mvc.Result]。两者能够提供相同的 HTTP 契约,但类型和组合方式并不相同。把一种 API 的 Result 直接赋给另一种,在本章的负例中会编译失败。 类型化请求还解决了另一个具体问题:身份信息如何经过 Action 组合进入异步计算。实验中的 Java 和 Scala 实现都切换到名为 api-worker 的线程,普通 ThreadLocal 没有随之传播,显式捕获的用户却保持正确。这个结果来自两个独立版本工程的真实生产 HTTP 请求。 固定两个组合,先比较 HTTP 契约 主对照固定 Play 3.0.6、Scala 2.13.15、sbt 1.10.7、JDK 21.0.11。另一个独立工程固定 Play 3.0.6 与 Scala 3.3.4,运行相同 Java、Scala 源码和同一套网络矩阵。Scala 3 构件实际包含 play_3-3.0.6、play-j...
深入 Play 35:可验证的订单与订阅服务
订单创建返回 200,响应中却包含 notification=BUDGET_FAILED。数据库订单已经提交,固定本地下游超过了 120ms 请求预算;这两个结果同时成立。把通知超时转换成“订单失败”,会诱导调用方换一个幂等键重试,产生第二笔合法订单。 结课工程把这种边界放进一个真实服务:PostgreSQL 保存订单和库存,Play 签名 session 提供合成身份,对象策略同时约束 tenant 与 subject;SSE 查询订单状态,CSV 通过文件源下载。每项结论都有对应输入、数据库或资源终态,以及可重跑的有限实验。 工程范围与可复核产物 结课服务已接入第00篇的累计源码包。共享55项JUnit全部执行,112个应用及生成class与stage jar字节一致;生产重放再次验证订单、库存、幂等、导出、订阅与在途停机,见 evidence/batch30-35/shared-capstone-http。实验仅删除自己的UUID schema,保留共享PostgreSQL实例。 本篇固定 Play 3.0.6、JDK 21.0.11、PostgreSQL 17.6。Pl...
深入 Play 34:故障诊断,从外部症状追到线程、提交与资源终态
健康检查返回 200,并不说明服务没有线程饥饿;请求返回 504,并不说明订单没有提交;流的终止回调已经执行,也不说明应用持有的文件句柄已经释放。这三个故障需要查询不同的证据,HTTP 状态码无法独自回答它们。 独立实验包包含 A、B、C 三个可选故障 profile。每个包先保留缺陷,再用相同请求序列运行对应修复。累计示例工程不引入这些缺陷。实验的入口是现象记录:请求耗时、线程栈、独立 SQL、文件描述符和下一次资源申请。 匿名故障包和诊断边界 应用固定为 Play 3.0.6、Scala 2.13.15、sbt 1.10.7、Corretto 21.0.11。数据库场景使用 HikariCP 5.0.1、PostgreSQL JDBC 42.7.5 和 PostgreSQL 17.6。所有请求都发给实际 stage 生产进程,监听地址为 loopback。 源码和驱动位于 play-electives/diagnosis。A/B/C 是给故障包的中性编号,阅读练习可以先看观察结果,再展开源码。自动回归驱动知道预期断言,因此这里的“盲测”是隐藏根因的诊断练习,不是声称由不知情...
JPA 持久化 01:实体身份与工作单元
两次 find 命中一行,拿到的是同一个对象吗 审批页面加载采购申请,后台也按相同 ID 加载。它们持有的 ProcurementRequest 可能代表同一数据库行,Java 引用却不是同一个。如果页面把自己持有的旧对象从上下文移走后改成 SUBMITTED,数据库不会仅凭主键相同自动更新。这不是 Provider 忘记写库,而是“相同数据库身份”与“当前上下文正在管理该引用”属于两个问题。把 == 的结果当成整个进程的身份保证,会在跨请求缓存、重试和审批工作单元里产生隐蔽的旧状态。 本篇源码、迁移、脚本与测试可从固定版本完整归档取得;版本 1b08ada,SHA-256 见源码清单。 本章只谈采购申请的生命周期,使用累计工程的 purchase_request 与 purchase_line。00 篇解释 SE 持久化单元启动;完整源码入口在 examples/jpa/。这个工程使用 PostgreSQL 16 独立数据库 jpa_lab,RESOURCE_LOCAL,Java 21,Hibernate ORM 7.1.36.Final,JPA 规范版本 3.2。已有 6 项...
深入 Play 33:相同约束下的 Web 栈,异步返回与事务终态
一个订单请求收到 504,数据库随后插入订单并扣减库存。把控制器返回类型从 CompletionStage<Result> 换成 Mono<ResponseEntity<?>>,这条因果链仍然存在;把 HTTP 入口换成 Spring MVC,也不会自动回滚已经运行的事务。 Web 栈比较需要把控制器适配、业务排队、数据库事务和 HTTP 响应分开。框架负责什么,业务代码自己提供什么,要在负载结果之前说明。否则,一套应用使用两个 JDBC 连接,另一套使用几十个连接,得到的吞吐差异主要来自资源配置。 三个独立进程,一份业务代码 实验包含 Play 3.0.6、Spring Boot 3.3.6 MVC 和 Spring Boot 3.3.6 WebFlux 三个小工程。源码位于 play-electives/comparison/{play,mvc,webflux}。三份 comparison.Kernel 的源码 SHA-256 相同;控制器和应用启动适配层单独实现。 可下载三个工程及运行证据,用SHA256SUMS校验。共享目录重新构建后...






