深入 Play 08:BodyParser 的类型、字节限制与请求体消费
同样是 32 字节上限,已知 Content-Length 为 35 的 JSON 请求在本批得到 413,parser 字节计数为零;没有 Content-Length 的分块请求也得到 413,却已经向计数流交付了 35 字节。长度声明可以让拒绝提前,实际字节限制仍需要流级检查。 parser 成功也不代表业务输入有效。显式 JSON parser 接收空请求体后,本版得到 null 并进入控制器;默认 parser 的空请求则返回 empty 表示。接口需要自己决定空输入能否接受,不能把两种入口的结果合并为“JSON 已经验证”。 BodyParser 返回消费器而非立即返回对象 Java BodyParser 的核心接口是: 12Accumulator<ByteString, F.Either<Result, A>> apply(Http.RequestHeader request); 它接收请求头,返回一个可以消费 ByteString 的 Accumulator。正文可能还在到达,不能把 apply 返回解释成已经取得整个输入对象。 E...
深入 Play 07:Filter 在路由、解析错误和业务失败之间怎样执行
未知路由返回 404,JSON parser 拒绝返回 400,实验 Gate 提前返回 403,这三种响应都带有外层 Filter 加上的头。控制器同步抛出或返回失败 Stage 时,客户端得到 500,却没有执行相同的成功回调。 状态码属于 HTTP 响应;Stage 成功或失败属于异步计算。二者不能混为一谈。一个 400 Result 可以由成功完成的 Stage 传给 Filter,而失败 Stage 在服务器错误处理中被转换为 500,未必再经过 Filter 的 thenApply。 Router 先选 handler,Filter 再包装 DefaultHttpRequestHandler 的普通请求路径,先执行 routeWithFallback,再调用 filterHandler 包装选定的 handler。选不到普通路由时也会构造处理未匹配请求的 handler,因此 Filter 有机会覆盖应用层的 404。 1234567RequestHeader → routeWithFallback → selected handler / fallback h...
深入 Play 06:Action 组合怎样决定鉴权与审计的执行边界
合法 JSON 在默认路径先经过 parser,再进入审计和鉴权;同一条路由开启 deferred parsing 后,鉴权可以在 parser 之前返回 401。本批真实 HTTP 实验中,未授权的畸形 JSON 分别得到 400 与 401,业务计数均为零。两种结果对应不同执行边界。 Action 组合适合表达某一组控制器操作的公共条件,但注解本身不能证明拒绝请求的代价、审计覆盖范围或失败处理。需要检查 delegate 链的顺序,以及请求体解析发生在这条链的什么位置。 一个可以观察的 delegate 链 累计工程新增 PipelineController。同一个方法通过两条 routes 入口调用,JSON parser 限制为 32 字节,业务方法返回 CompletionStage<Result>: 123456789101112131415@With({AuditAction.class, AuthorizationAction.class})@BodyParser.Of(CountingJsonParser.class)publi...
深入 Play 05:Guice 怎样构建对象与配置边界
在 injector 上连续取得两个未标单例的控制器,本批测试得到两个不同对象;向同一条路由发送 16 个并发请求,却观察到同一个控制器引用。这两个结果同时成立:Guice 决定每次注入怎样构造对象,生成 Router 决定已注入的引用如何被后续请求使用。 因此,“控制器没有 Singleton 注解,所以每个 HTTP 请求新建一个控制器”不是可靠结论。需要分别检查对象作用域、持有关系和请求调用方式,再讨论字段能否安全共享。 从接口到构造器依赖图 示例用一个库存接口把查询边界从控制器中抽出: 123public interface InventoryService { int stock(long productId);} 实现从配置读取合成库存,构造时拒绝负数: 12345678910111213141516@Singletonpublic final class ConfiguredInventoryService implements InventoryService { private final int stock; ...
深入 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 字段的处理边界。...





