深入 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 | Corretto 21.0.11+10-LTS | 启动日志与 java -version |
| Play / sbt 插件 | 3.0.6 | project/plugins.sbt、产物中的 Play jar |
| Scala / sbt | 2.13.15 / 1.10.7 | 构建定义与启动日志 |
| 服务器 | Play Pekko HTTP backend 3.0.6 | 生产包的服务器模块 |
| Pekko / Pekko HTTP | 1.0.3 / 1.0.1 | 生产包实际解析的 jar |
上游 release 构建定义的 sbt 基线是 1.10.5;本实验冻结在 1.10.7,并已实际构建。这两个数字分别描述上游构建和示例构建,不能交换。上游版本定义 可独立核对。
Play 的 Java 应用仍需要 Scala 编译器。routes 与 Twirl 会生成 Scala/Java 代码,Play 内核也有 Scala 实现;“业务代码写 Java”并不等于“构建过程中没有 Scala”。构件坐标使用 org.playframework,流接口使用 org.apache.pekko。不要把旧版 com.typesafe.play、Akka 包名和当前示例拼成一个未经验证的组合。
工程里每个目录承担什么
下载首批完整工程与证据。在博客源码仓库中,它对应 examples/play-lab/:
1 | |
project/ 中的构建定义由 sbt 使用;app/ 是 Play 应用源码;conf/ 提供运行配置与 routes;test/ 是测试源码。库存接口放在一个实际被应用消费的普通子项目,目的是观察 classpath 依赖,不是预先建立微服务架构。target/ 是生成产物,改源码后由构建器重建,不能把手改生成路由当成接口修改。
三个固定版本入口足以描述工具链:
1 | |
project/build.properties 中固定 sbt.version=1.10.7。下载包保留了完整 build,包括测试依赖。这里的 Scala 是构建定义语言,不能把 lazy val root 复制进 Java 控制器。
Guice 依赖是显式加入的。Play 插件、依赖注入实现和 HTTP 服务器不是同一个职责;官方构建说明 也分别展示它们。当前插件组合选择了 Pekko HTTP 后端,后文的 HTTP 实测只适用于这个已记录组合。
第一条响应从哪里来
最小成功路径是声明路由、编译控制器、启动应用、发送请求:
1 | |
1 | |
routes 是编译器输入,不是服务器每次收到请求后解释的一段任意字符串。它会生成引用控制器方法的代码;方法不存在、参数类型不兼容时,可能在编译阶段失败。路径和参数匹配则发生在运行时,因此 GET /missing 的 404 不能与 Java 编译错误混为一谈。
ok 构造一个状态为 200、携带文本实体的 Result。控制器没有直接写 socket。服务器后端把这个结果转为 HTTP 响应并消费实体,客户端才读到头和正文。后续第 02、04 篇会用延迟流验证这两个时点的区别。
本批还提供 GET /orders/42,返回 id、verbose 与合成库存。构造器注入 InventoryService,由模块选择实现。先验证 /health 可以把路由与启动问题缩到很小的范围,再检查带依赖的业务接口;直接从复杂订单失败推断“Play 没启动”,会遗漏服务绑定和配置错误。
在本机重跑,而不是只看输出截图
先设置已有的 JDK 21 路径,确认指向 JDK 根目录:
1 | |
本次下载的 launcher SHA-256 是:
1 | |
该值用于核对本实验工件,不代替下载来源的信任判断。脚本 sbtw 默认读取这个本地 jar;也可通过 PLAY_LAB_SBT_LAUNCHER 指定已有 launcher。它要求显式设置 JAVA_HOME,避免机器默认 Java 8 导致构建和文章版本不一致。
1 | |
首次运行需要下载 sbt、插件和依赖,网络条件会影响耗时。HTTP 脚本只监听回环接口,并寻找空闲端口;启动进程、请求断言、保存日志和退出均由脚本完成。运行后没有需要手工寻找的后台服务器。空闲端口探测与绑定间仍有短暂竞争窗口;端口被其他进程抢占时应查看启动日志,不把连接到错误进程的结果当作成功。
--dev 走 sbt run;默认走 target/universal/stage/bin/play-lab。两者使用同一请求矩阵,但生命周期不同。开发模式包含构建链接与重新加载行为;生产模式运行已打包的 jar,不需要在请求过程中重新编译源文件。生产部署说明 给出了 stage/dist 的用途。
脚本每次生成临时应用签名密钥,通过配置中的 ${?APPLICATION_SECRET} 注入。仓库里的固定密钥只服务于离线合成实验;真实部署必须提供自己的密钥。这里没有实际用户凭据或业务 Cookie。
哪些结果已经被证明
本批的 JUnit 测试通过 7 项。测试覆盖类型化路由、JSON 解析短路、请求和结果复制、模板转义、Guice 作用域与替换,以及配置/绑定/循环依赖失败。它们使用应用内测试请求,能验证调用链的一部分,但不能证明 TCP 端口、HTTP 编码和生产包完整性。
开发模式与生产产物分别通过 56 次真实 HTTP 请求,验证了 200、201、204、400、404、413、415 和 500,保留响应头、正文及时间。同步抛异常和失败的 CompletionStage 均经过真实服务器;Cookie 的设置、读取、签名篡改和 flash 删除也通过客户端核对。16 个并发请求观察到当前路由持有相同控制器与库存实例,第 05 篇会解释为何这不等于“所有未标单例的对象都是单例”。
生产进程在脚本完成后收到 SIGTERM,退出码记录为 143。这是实验的受控退出,不是进程在接受请求时异常崩溃;它也不能证明有数据库连接或长任务时的优雅退出,当前工程尚未引入这些资源。
--negative 在临时副本中改变路由参数类型、模板参数类型和模块依赖,要求编译非零退出,并检查日志中存在编译错误。下载失败同样会使命令非零退出,所以只看退出码不够。原始编译日志保存在 evidence/batch00-05/negative-*.log,第 01 篇逐项分析。
这一基线尚未验证 JDBC、事务、生产容量或跨服务器后端一致性。进入异步和持久化章节前,先保留这个已能独立重跑的起点:若新增依赖后构建或 HTTP 失败,可以比较同一工程的变更,而不必重新猜测工具链。
