Java EE 企业应用 00:从空目录到最小 WAR,健康响应证明了什么
健康接口返回了 ready,采购系统就能交付了吗 一个开发者拿到采购审批应用,第一件事通常是运行服务。如果浏览器显示 ready,容易把它理解成“系统已经启动”。可是采购申请能否提交、订单是否只生成一次、其他租户能否读取申请,都没有经过这个接口。这里需要先把“从空目录运行”拆成可检验的阶段:命令行能编译源码;服务器能加载单个 WAR;HTTP 能抵达健康资源;业务事务能提交;权限和业务结果能被核对。第 00 章只走到前三项,不用健康响应冒充后面三项的证明。 本篇源码、迁移、脚本与测试可从固定版本完整归档取得;版本 20261005-local(基于 1b08ada),SHA-256 见源码清单。 教学对象是单 WAR 的企业采购系统。实际采购还需要申请明细、审批记录、订单、通知和审计;本章只检验健康接口。当前工程另有未认证的 /api/db-check 诊断入口,且在 JAVAEE_DEMO_MODE=true 时才开放未认证的 /api/lab/requests 教学入口;二者仅供监听回环地址的隔离实验使用,不能作为生产服务对外暴露。先修自测也因此很具体:能解释 HTTP 20...
深入 Play 23:事务与幂等,数据库提交如何关联 HTTP 结果
两个请求都收到 HTTP 504,一个对应的订单尚未提交,另一个对应的订单已经提交。外层响应预算耗尽时,数据库操作可能处于不同阶段;重试若直接生成新订单,就会把“没有收到成功响应”误当成“没有发生写入”。 事务负责一组数据库修改的原子性,幂等键负责把多次请求关联到同一次业务操作。响应超时、任务完成和事务提交需要分别观察,单独检查 Stage 或 HTTP 状态不能替代数据库终态查询。 withTransaction 等待同步 block 返回 Play 3.0.6 的 Databases.scala 把连接设为非自动提交,调用 block,取得返回值后 commit;普通异常路径 rollback,外层 withConnection 再关闭连接。 返回值的泛型没有改变这一时序。若 block 返回 CompletionStage<T>,这里取得的是 Stage 对象,源码不会等待它完成。事务边界包住的是同步调用 block 的过程。 sequenceDiagram participant Worker as 工作线程 participant Tx a...
深入 Play 22:JDBC 与连接池,异步接口为何仍会等连接
连接池最多提供两个连接,四个线程同时执行 JDBC:两个执行 SQL,另外两个停在借连接的位置。把执行器缩为两个线程后,连接池等待者变成零,执行器队列里多出两个任务。请求总数没有改变,等待发生的位置变了。 这种区别决定了排障时应检查什么。返回 CompletionStage<Result> 只能说明结果稍后交付;借连接和执行 SQL 仍会占用执行它们的线程。只记录 HTTP 总耗时,无法区分排队、借连接、数据库执行和响应调度。 从提交到归还,分别记录时间 累计工程 play-lab/app/labdb/PoolLab.java 为每个任务记录提交、开始执行、开始借连接、借到连接、SQL 开始、SQL 结束和归还连接的相对微秒数。所有时间来自同一次实验的单调时钟起点,不用不同机器的墙上时间相减。 flowchart LR Submit[提交任务] --> Queue[执行器队列] Queue --> Start[工作线程开始] Start --> Borrow[等待连接池] Borrow --> SQL[执行 J...
深入 Play 21:上传下载、临时文件与 IO 关闭
一个 multipart 请求声明 50000 字节,实际发送文件前缀后就关闭 TCP。断连前,临时目录里已经存在 part 文件;断连后,controller 没有机会处理完整请求,但 parser 的失败路径清理了文件,FileIO 的开始与完成计数均为一。 把删除操作只放在 controller 的 finally 中,覆盖不了这个生命周期。临时文件在 body parser 阶段创建,清理责任也必须从这个阶段开始,直到成功移交给应用或失败终止。 请求尺寸、文件尺寸与路径是三个约束 文件接口接收的不只是文件字节。multipart 请求还包含边界、字段头、普通表单项和其他文件部分,因此“单个文件不超过 64 KiB”与“整个请求不超过 64 KiB”不是同一个限制。实验为整个 multipart body 设置 65536 字节上限,同时只允许一个文件。 Content-Length 提供提前拒绝的机会,但不能作为唯一依据。chunked 请求没有可直接使用的总长度,parser 必须在消费字节的过程中累计。若只在 controller 中读取文件大小再比较,请求可能已经...
深入 Play 20:WebSocket 握手、消息上限与连接生命周期
同样一个 65537 字节的 WebSocket 帧,用一次 socket 写入发送时,本次实验观察到应用的 4096 字符限制报错;改成每次写 8192 字节,服务端返回了 1009 关闭码。这里变化的是 TCP 写入与后端帧聚合路径,业务消息内容没有变化。 因此,验证 WebSocket 限制需要同时记录帧的构造、消息分片、应用处理量和关闭终态。只看到配置里有一个 64k,或者只看到连接已关闭,都不足以定位到底由哪一层拒绝了输入。 握手以前能拒绝,握手以后要管理流 WebSocket 连接先经过 HTTP 握手。实验使用 WebSocket.Text.acceptOrResult,验证合成 token,再验证精确 Origin。错误 token 返回 401,错误 Origin 返回 403;两条拒绝路径都在创建连接账本与 Flow 之前结束。 “拒绝没有分配应用流”需要证据。脚本先发送真实 Upgrade 请求,读取 HTTP 状态,然后查询同一个 id 的账本。两次查询均不存在记录。若在认证以前先建立 Actor、队列或数据库订阅,即使最终返回 401,仍需另外处理已经...
深入 Play 19:SSE 编码、重连游标与慢客户端
客户端收到 id 为 101、102、103 的三个事件后关闭连接,再携带 Last-Event-ID: 103 建立新请求,得到 104、105、106。这次连续性来自服务器保存的事件集合与游标选择规则;仅给事件加一个递增 id,无法提供相同的重放能力。 同一个有限事件源经过两种本地代理模式时,首个事件的交付时间也不同。立即转发模式几乎立刻交付,按两秒条件刷出的缓冲模式约等待 2.279 秒。控制器已经生成事件,不代表下游每一层都已把事件交给客户端。 一条事件可以跨越多个读取操作 Server-Sent Events 使用文本格式表达服务端事件。应用需要同时处理 HTTP 传输边界和 SSE 事件边界。网络上的一个 read 可能只得到半行,也可能包含多个事件;使用“一次 read 对应一个事件”的解析方法会把 TCP 分段误当业务分段。 本篇的每个 sample 事件包含两行 data、一行 id 和一行 event,空行结束事件。首次消息格式如下: 12345event: sampleid: 101data: line-one-101data: line-two-101 ...
深入 Play 18:HTTP 流式响应、背压与资源终止
一个响应最多输出 32 MiB,每次只产生 16 KiB。客户端收到响应头以后暂停读取,服务端生产计数停在 49 块;客户端随后只读一块就关闭真实 socket,服务端流进入终态,关闭计数为一。 这个实验测到了当前链路中的需求反馈与取消传播。49 块已经远大于客户端实际读取的一块,差额可以停留在流、服务器和内核缓冲中。把应用 Source 的 inputBuffer 设成一,不能把整条网络链路理解成“只允许提前产生一个元素”。 生产速率、消费速率与待发送字节 流式响应首先改变的是生产方式。有限列表先把所有对象构造完再交给 Source,虽然输出按元素传输,列表占用的内存已经发生。实验的 Source.range 只产生整数索引,map 在获得下游需求时分配一个 16384 字节数组,上限为 2048 块,不构造完整字节列表。 先忽略对象开销,以字节计算未消费数据。假设生产速率为每秒 8 MiB、消费速率为每秒 1 MiB,持续两秒且生产端没有减速,待发送数据会增加 14 MiB。这是速率差的积分,不是已经测出的 JVM 堆占用。如果可用缓冲只有 512 KiB,差额不能永久累积...
深入 Play 17:缓存加载、失效与并发回源
四个线程同时请求同一个不存在的缓存 key。加载函数返回一个尚未完成的 Future,四个调用者都在等待,但加载计数只有一。完成这个 Future 后,四个调用者得到相同的 loaded;随后热读仍没有增加加载次数。 这不是从 AsyncCacheApi 的名称推出来的保证。实际使用的 Caffeine provider 把进行中的加载结果放进同一个 key 的槽位,后续调用共享该结果。更换 provider、换成两个 JVM 或改成先 get 再 set,都需要重新验证回源次数。 key、值与进行中的加载 本篇使用 Play 3.0.6 的 caffeine 模块和解析得到的 Caffeine 3.1.8,累计工程基于 JDK 21。CacheLab 注入 AsyncCacheApi,以唯一实验前缀隔离 key,结束时只删除这组 key,避免清空应用共享缓存。 状态 调用者可见结果 本实验加载计数 冷 key,加载未完成 四个 Stage 均 pending 1 完成受控加载 四个调用者均得到 loaded 1 同 key 热读 loaded 1 另...
深入 Play 16:WS 客户端的响应消费与请求预算
本地下游先发送响应头和 prefix-,随后等待闸门。普通 get 的 Stage 仍未完成,stream 的响应 Stage 已经完成,而消费响应体的 Stage 仍在等待。释放闸门,服务端写入 suffix,两条路径最终得到相同的 13 字节正文。 同一个 HTTP 响应具有不同的可观察完成点。调用者如果把“拿到 WSResponse”理解为“所有字节已消费、连接已可复用”,在流式路径上就会提前结束资源管理。超时放在哪里,也取决于它要覆盖哪个完成点。 固定客户端后端与实验输入 累计工程使用 Play 3.0.6、JDK 21,通过 javaWs 注入应用级 WSClient。解析产物为 standalone WS 3.0.6;其源码提交固定为 8380c047c5cf19182be56bdb831a28c9724e87bc。这里讨论的是该版本的 AHC 实现,不把 AsyncHttpClient、JDK HttpClient 或其他 WS 后端视作可互换语义。 底层构建记录对应 AsyncHttpClient 2.12.3 和 netty-reactive-streams ...
深入 Play 15:超时、取消与重试的任务终态
一个任务已经进入工作线程,随后停在受控闸门前。给它包装 100 毫秒超时,包装 Stage 以 TimeoutException 结束;读取原任务,done 与 cancelled 却都是 false。释放闸门后,原任务返回 late-success。 这组结果区分了两个事件:调用者停止等待,工作本身完成。若工作在完成前还会扣库存或提交事务,前一个事件不足以证明后一个事件没有发生。超时处理需要同时回答返回什么、谁停止工作、哪些副作用已经发生。 一次调用有多个终态 本篇沿用 Play 3.0.6、Scala 2.13.15、JDK 21 的累计工程。app/labbudget/TimeoutLab.java 使用合成任务、CountDownLatch 和有限工作线程,把迟到完成、协作取消、重试分类放在同一组确定时序中。实验不访问数据库,也没有真实订单。 观察对象 可直接证明的事实 不能据此推出的事实 超时包装 Stage 等待结果已变成失败 原任务已停止 原任务 Stage 值或异常已完成 外部事务必然回滚 取消标记 取消意图已发布 执行方已经检查标记 ...






