深入 Play 14:请求属性、MDC 与类加载器怎样跨线程
Alice 与 Bob 的合成请求先在执行器A处理,再到执行器B;Bob的第二步先执行并抛错,Alice随后成功。每个阶段记录的用户、请求id、Request属性和MDC均一致。两条工作线程再被复用时,只剩各自原有的 worker 标记,没有上一次请求的身份。 另一组对照只使用 Play 的 ClassLoaderExecution:工作线程看到被捕获的类加载器,却没有看到调用线程的 callerOnly MDC。执行上下文传播了哪些值,需要按实现与实验逐项说明。 显式请求值与线程状态 Request是业务方法的显式输入;MDC是与当前线程相关的日志状态;线程上下文类加载器又是另一种线程状态。切换执行器后,Request引用仍可传给回调,但线程相关状态不会仅因引用存在就自动复制。 实验把两个合成身份关联到从原请求派生的新请求,并构造不可变上下文记录: 12private record RequestContext(Http.Request request, String requestId, String user, M...
深入 Play 13:阻塞执行位置、有限队列与拒绝策略
专用 dispatcher 配置两条线程、一个队列位置。两条任务正在等待,第三条排队,再提交第四条时,实验没有收到预期中的拒绝异常:第四条在提交它的协调线程上执行,业务在途数从2增到3。 相同规模的 JDK ThreadPoolExecutor 改用 AbortPolicy,第四次提交抛出拒绝异常,应用将它映射为503。有限队列与明确拒绝,是两个需要分别验证的条件。 执行位置首先是实际线程 第12篇已经区分调用返回与工作线程。本篇进一步比较默认 dispatcher 和命名 lab-blocking-dispatcher。受控任务进入后记录线程名、增加在途计数,再等待独立协调线程释放。 默认任务实际运行在线程名包含 default-dispatcher 的位置;专用任务包含 lab-blocking-dispatcher。两个位置的 inFlightBeforeRelease 都为1。这个结果证明当前注入和配置确实选用了不同执行位置,不证明它们拥有独立 CPU、数据库连接或下游资源。 命名执行上下文沿用 Play 提供的类: 123456public final class L...
深入 Play 12:CompletionStage 的依赖、线程与失败传播
CompletableFuture.completedFuture(blockingCall()) 返回已经完成的 Future,blockingCall 却在包装之前就执行了。本次受控实验中,任务停在 latch 时,调用方法的线程仍未返回;改成显式执行器的 supplyAsync 后,调用已返回,结果 Stage 仍未完成。 两种写法都有异步类型,执行位置和调用返回时点却不同。讨论 Play 的异步接口,需要同时记录任务何时启动、在哪个线程执行,以及返回的结果何时可用。 返回类型不改变已经发生的调用 Play 的 Java Action 接受 CompletionStage<Result>,但在得到 Stage 前仍需要调用业务方法。JavaAction先调用 firstAction.call,再把返回的阶段转换并展开。业务方法内部已经执行的工作,不会因为外层展平而改成另一个执行器上的工作。 实验使用独立调用线程 lab-caller 和工作线程 lab-worker。blocking 方法记录进入,等待有限 release latch,再返回线程名: 1234...
深入 Play 11:Twirl 类型、输出转义与静态资源缓存
同一段包含 img 和 script 的输入,作为 String 输出时,浏览器中的事件标记保持 none;包成 Html 后,标记变为 script,img。两条请求都成功编译并返回200。编译期类型检查与浏览器执行安全,检查的是不同问题。 资源缓存也需要检查实际产物:本例的 Assets.versioned 反向路由生成普通 lab.css 路径,生产响应为 max-age=3600,并没有文件摘要后缀。方法名中的 versioned 不能代替摘要生成插件和产物验收。 模板参数进入构建过程 Twirl 模板是生成 Scala 源码的输入。本篇冻结 Play 3.0.6 和 Twirl 2.0.7;累计工程的 contentTwirl.scala.html 声明: 1@(payload: String, raw: Boolean) 控制器调用生成的 views.html.contentTwirl.render(PROBE, raw)。参数、表达式和生成代码共同参与编译,因此模板并不是运行时随意填入字符串的文件。第01篇的生成源码可以辅助检查调用签名;实际编译成功才证明当前调用...
深入 Play 10:表单绑定、跨字段验证与库存竞争
quantity=1&quantity=5&confirmation=1 进入普通 Form 绑定后,当前实验取得 quantity=1,且没有验证错误。调换两个 quantity 的顺序,把 confirmation 改成5,绑定值也变成5。标量值已经被选择,后续验证器无法据此判断原始字段是否重复。 另一个问题发生在验证之后:两个输入完全有效的请求争抢一份库存,得到 201 与409。表单有效只说明当前输入满足约束;库存是否仍可扣减,由执行业务操作时的共享状态决定。 从请求体到业务结果 本篇使用 Play 3.0.6 的 FormFactory、Form 与 Bean Validation 集成。实验有三条不同入口:GET /content/form 生成带 CSRF token 的页面;POST 同一路径执行严格表单检查和预约;POST /content/form/ordinary 只观察普通绑定,不改变库存。 阶段 负责的问题 当前失败表达 urlencoded parser 媒体类型与请求字节 parser 拒绝 原始字段检查 未知字段...
深入 Play 09:JSON 字段、金额精度与错误契约
向同一个接口提交 {"name":"Ada","amount":"12.34"} 与 {"name":"Ada","amount":12.34},实验分别得到 201 和 400。两个请求都是合法 JSON,区别来自接口对金额表达的约定:只接受受限十进制字符串。 另一个请求包含两次 name,{"name":"first","name":"last","amount":"1"},结果却是 201,输出 name 为 last。控制器检查了字段白名单,仍无法从已经折叠的树恢复原始重复键。语法、字段、数值与原始 token 是不同的检查位置。 接口先约定输入和输出 本篇沿用 Play 3.0.6、Scala 2.13.15、JDK 21 的累计工程,入口为 POST /content/json。它只返回一个合成回执,没有订单写...
深入 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; ...





