深入 Play 04:Request 与 Result 的状态边界
把请求属性加入 Request,或给 Result 加一个头,都会得到新的对象;忽略返回值,后续代码可能继续使用旧状态。把用户名字写入 session,数据出现在浏览器 Cookie 中;签名阻止篡改,却没有让名字不可读。拿到一个流式 Result,也只说明响应描述已经存在,尚不能说明客户端收完了正文。 这三组边界分别涉及对象更新、客户端状态和实体消费。它们共享一个原则:对外观察需要明确时间与载体,不能用“请求成功了”概括所有变化。 Request 保存了什么,更新影响谁 Play 的 Http.Request 包含方法、路径、头、属性以及已解析的请求体等信息;Http.RequestHeader 不要求持有已解析的 body。第 02 篇默认 parser 成功后,才把 body 放进交给业务方法的 request。 对象更新接口返回新请求: 123456TypedKey<String> key = TypedKey.create("lab-key");Http.Request original = fakeRequest(GET, "...
深入 Play 03:类型化路由的绑定与反向生成
GET /orders/42 能调用 order(long, boolean),依赖的不是控制器里手写 Long.parseLong,而是生成路由选择了对应的参数 binder。将输入换成 /orders/no,控制器还没执行就得到 400;将路径换成 /missing,则没有这个绑定过程,直接进入 404 fallback。 类型化路由把接口声明和代码调用连接起来,但仍有运行期错误边界。缺失值、非法值、重复 query 与百分号编码需要分别验证。尤其当参数是业务标识或权限字段时,能转换成一个 Java 值不代表请求没有歧义。 路由声明里哪些值来自请求 当前工程包含两个对照路由: 12GET /orders/:id controllers.LabController.order(id: Long, verbose: Boolean ?= false)GET /reverse controllers.LabController.reverse(id: Long) 第一条的 id 来自 path capture,verbose 来自 query,并有缺省值 false。第二条...
深入 Play 02:一次请求在哪个阶段失败
同一个订单接口可能返回 400、413、415 或 500。只看到状态码,无法知道业务方法是否执行:400 可能来自路由参数绑定,也可能来自 JSON 解析;500 可能来自同步抛异常,也可能来自异步结果失败。排查需要把输入、阶段事件和客户端观察对应起来。 当前工程在过滤器入口、控制器入口以及结果 Stage 的成功或失败处分别打点。它没有给每个源码函数加日志,而是用最少的事件回答一个问题:请求已经越过哪些边界,在哪个分支停止? 先找到 handler,再包装过滤器 Play 3.0.6 的默认请求处理器先选择 handler,再为可过滤的 action 包装过滤器。这一点影响读源码的方式:过滤器的执行入口早于控制器,不意味着 Router 对 handler 的选择也发生在过滤器之后。 固定的 DefaultHttpRequestHandler.handlerForRequest 依次调用 routeWithFallback、Handler.applyStages、filterHandler,再处理一次预处理阶段。filterHandler 为应用上下文内的 Essentia...
深入 Play 01:sbt 怎样生成路由与模板
把 routes 中的订单号从 Long 改为 String,控制器方法却仍接收 long,错误通常在编译时就会暴露。把模板参数从 String 改为 Int,调用处也必须修改。Play 将这些约束放进生成代码,再交给 Scala 与 Java 编译器;这让一部分接口错误不必等到请求到达时才出现。 这一机制不能验证订单是否存在、调用者是否有权限,也不能保证每个 URL 都匹配。编译期检查负责代码之间的调用契约,运行期 binder 和业务逻辑负责输入与状态。理解 sbt 的任务和 classpath,才能判断失败属于哪一层。 构建定义也是代码,但运行位置不同 首批 累计工程 固定 sbt 1.10.7、Play 插件 3.0.6 和 Scala 2.13.15。目录中的 build.sbt 是 Scala 风格的 sbt 构建定义,不是应用运行时的业务源码。project/plugins.sbt 将 Play 插件加入构建;它不会因为应用里 import 了 play.mvc.Result 就自动出现。 以下两个表达式承担不同用途: 12ThisBuild / scalaVer...
深入 Play 00:从固定版本到第一条真实响应
Play 应用能通过编译,客户端却仍可能收到 404;测试能调用控制器,生产包却可能缺少模块;控制器返回了 Result,响应正文也可能尚未发送完。这些问题处在不同阶段,需要分别取证。 本系列从一个只有合成库存数据的订单接口开始。第一批使用同一工程补充路由、模板、请求解析与依赖注入,后续再加入数据库和下游服务。当前的 stock=7 是配置值,没有数据库连接,也没有库存扣减;不能用一次查询成功证明事务正确。 固定一个可以追到源码的构建 教学基线采用 Play 3.0.6,不表示它是当前最新补丁或生产升级推荐。版本选择的目的,是让正文的类、默认值和错误路径对应同一份实现。该 release 对应源码 commit 2e56aff7d4e7a74af61e4bd39ec9e3ed7f300cd6。Git annotated tag 自身也有对象 ID;源码链接使用 tag 指向的 commit,避免把两者混用。 本次实际运行组合如下。JDK 是本机已有的 Amazon Corretto,没有切换系统默认 Java。 对象 实际版本或选择 核验位置 Java Corre...
深入 Hibernate 06:merge 复制到哪个对象
脱管订单的金额已经变成 20.00。执行 session.merge(order) 后,再把原引用改成 99.00,提交时数据库应该保存哪个值?决定结果的是修改发生在哪个 Java 对象上。对于本篇已存在数据库行的脱管对象,merge 把状态复制到当前持久化上下文中的托管对象,并返回该对象;原引用仍然脱管。 实验沿用前面章节的 PurchaseOrder。运行基线是 JDK 21、Hibernate ORM 7.1.36.Final 和真实 PostgreSQL;源码固定为 ca7715d7c1afbd46d518752115848bffd9322413。本篇区分 API 对对象状态的承诺、冻结实现的复制过程和本地数据库实验结果。 输入引用与返回引用 Session.merge(T) 的契约说明复制目标具有相同标识;上下文没有对应对象时需要加载,尚未保存的对象则保存其副本。它明确指出传入对象不会因为这次调用而与 Session 建立关联。Jakarta Persistence 3.2 §3.3.7.1 的实体合并规则规定脱管状态复制、关联级联以及未抓取 LAZY 字段的处理边界。...
深入 Hibernate 05:persist 返回时究竟完成了什么
有了订单 id,订单是否已经成立 订单接口创建金额为 12.34 的 PurchaseOrder,调用 persist 后立即取得主键。随后库存检查失败,事务回滚。调用方若只判断 id != null 就发送“下单成功”,会把一次未提交的写入当成业务事实。这个错误在序列和数据库自增主键下都可能发生,只是插入语句出现的时点不同。 本章要求的不变量是:只有提交成功、另一条 READ COMMITTED 连接可见订单行,才把持久化订单视为成立。Java 对象是否托管、是否取得主键、动作是否入队、数据库是否已经执行 INSERT,要分别记录。单表模型沿用第 03 篇,只增加测试专用的 IDENTITY 对照实体,不提前修改共享订单模型。 研究对象固定为 Hibernate ORM 7.1.36.Final、Jakarta Persistence 3.2、Java 21,源码 SHA 为 ca7715d7c1afbd46d518752115848bffd9322413。PostgreSQL 序列模型使用 allocationSize = 1;IDENTITY 模型只有主键和业务单号,没有关...
深入 Hibernate 04:Java 值怎样绑定到列
金额已经传入,为什么读回来变了 订单金额是 new BigDecimal("12.345"),数据库列是 numeric(19,2)。Java 对象可以保留三位小数,数据库列只能保留两位。调用 persist 的参数类型正确,不代表这两个数值域相同;写入成功也不代表重新查询能得到逐位相同的数。 这个问题包含三次转换:Java 值转换为 JDBC 可绑定的值,驱动把值交给服务器,服务器按实际列定义接受或拒绝它。注解描述映射,不是每次 setter 执行的校验器。@Column(scale = 2) 不会自动把 Java 字段改成两位小数,validate 也不能代替业务金额规则。 本篇固定 Hibernate ORM 7.1.36.Final 与 JDK 21。完整实验是仓库中的 examples/hibernate-lab/src/test/java/blog/hibernate/Chapter04Test.java,不需要从文中拼接代码。当前检出版本在专用 PostgreSQL 16.15 上三项测试通过;原始输出、JUnit 报告与另起连接的终态见 exa...
深入 Hibernate 03:托管、脱管和删除怎样影响订单
实验材料状态:下文保留先前 PostgreSQL 16.15 的原始记录。当前检出版本在专用 PostgreSQL 16.15 上复跑 00–03,6 项测试通过;新运行的报告在 examples/hibernate-lab/evidence/00-03/20261003T030855Z-pg16-cumulative/。主仓另保留 2026-10-02 的 PostgreSQL 17.6 原始运行;2026-10-03 同步后在 PostgreSQL 17.6 复跑 00–12,50 项通过,见 examples/hibernate-lab/evidence/00-12/20261003-pg17-synced-local/。两种环境的运行分别留证。 修改了对象,为什么订单金额没有变化 一个 PurchaseOrder 从数据库加载后,在业务方法里被 detach,随后金额字段从 5.00 改成 9.99。Java 对象显示 9.99,再次从同一数据库行加载却仍是 5.00。原因不是写入失败,而是这份 Java 对象已脱离持久化上下文,后续字段变化没有自动成为本次工作单元的...
深入 Hibernate 02:注解映射在启动时接受什么检查
实验材料状态:下文保留先前 PostgreSQL 16.15 的原始记录。当前检出版本在专用 PostgreSQL 16.15 上复跑 00–03,6 项测试通过;新运行的报告在 examples/hibernate-lab/evidence/00-03/20261003T030855Z-pg16-cumulative/。主仓另保留 2026-10-02 的 PostgreSQL 17.6 原始运行;2026-10-03 同步后在 PostgreSQL 17.6 复跑 00–12,50 项通过,见 examples/hibernate-lab/evidence/00-12/20261003-pg17-synced-local/。两种环境的运行分别留证。 错列名不该等第一笔订单暴露 创建订单接口引用了一个叫 does_not_exist 的数据库列,而真实 purchase_order 表没有该列。若应用启动只登记了 Java 注解却从未核查数据库,错误会延迟到写入或查询时才显露;订单服务的第一笔请求便充当了上线校验。第 00 篇建立的单表模型和第 01 篇的对象身份实验仍然适...





