HTTP 任务服务
目标不是再写一个 Spring 教程。JDK 21 的 jdk.httpserver 里有 com.sun.net.httpserver.HttpServer:绑定端口、按 URI 前缀挂 HttpHandler、用 Executor 管线程。文档写明:找不到 handler 就 404;不设 executor 就用默认实现。
代码在 src/com/aqjszz/tasks/,按 Java 21 写。下面讲设计,不维护第二份会漂的代码墙。
javac --release 21 -d out src/com/aqjszz/tasks/*.java
java --class-path out com.aqjszz.tasks.TaskStoreTest
java --class-path out com.aqjszz.tasks.Main 8080未命名模块能读 jdk.httpserver,不必写 module-info。命名模块要 requires jdk.httpserver。
一、目标与非目标
要解决的:
POST /tasks接单立刻 202,工人在另一条虚拟线程里跑(睡眠模拟阻塞 I/O)GET /tasks/{id}查状态;GET /health探活- 状态机在
ConcurrentHashMap上 CAS,不共享HashMap - 关停:shutdown hook 里
server.stop+ExecutorService.close
不解决的:落盘、鉴权、分布式 id、真正的 JSON RFC、对公网暴露。进程一关任务就没了。限流不靠给虚拟线程做池。
二、线程模型
每个 HTTP 交换一条虚拟线程:server.setExecutor(Executors.newVirtualThreadPerTaskExecutor())。JEP 444:Do not pool virtual threads。工人也是 exec.execute(() -> worker.run(id)),同一条 per-task 执行器,不是再开平台线程池。
Java 21 handler 里不要 synchronized 包着读 body / 写响应,否则钉 carrier。共享状态用 CHM。
HttpExchange 生命周期按 Javadoc:读方法、读头、读 body、设响应头、sendResponseHeaders、写 body、关。try (ex) 保证异常路径也关。Content-Length 用 UTF-8 字节 数,不是 String.length()。
绑定 127.0.0.1。setExecutor 必须在 start() 之前。
三、状态机
Task 是 record,分量 final。变化是 withStatus 出新实例,ConcurrentHashMap.replace(id, old, neu) 按 equals CAS。旧状态对不上就再读再试。putIfAbsent 建任务,禁止 get 再 put。
PENDING → RUNNING → DONE
→ FAILED
→ CANCELLED // InterruptedException 恢复中断标记后进来TaskWorker.work 里 Thread.sleep,虚拟线程会卸载。不要改成 synchronized wait。空 catch InterruptedException 会把取消吞掉。
id 用 UUID.randomUUID(),单进程够。CHM 不许 null value;record 里 result/error 为 null 表示没有,不把 null 当 map 空槽。
四、JSON 与路径
Json.payloadOf 只认 {"payload":"..."},转义只处理 \\ 和 \"。不是 RFC 实现。quote 按 code point 走。生产用库。
HttpServer context 是前缀:/tasks 会吃 /tasks/xyz。总 handler 看 getRequestURI().getPath(),精确 /tasks 走 create,否则当 get。/foo 接不到 /foobar。
五、关停与测试
server.stop(1) 的 1 是秒。ExecutorService.close()(19,AutoCloseable)shutdown 并等。虚拟线程卡在不可中断 native 上等不完——关停钩子的固有限制。
TaskStoreTest 不占端口:create → worker.run → DONE,缺 payload 抛 IllegalArgumentException。handler 的 400/404/405 起服务后用 HttpClient 打,端口 0 让系统分配。
压测时 dump 虚拟线程应在 sleep 上 park,carrier 大约核数,不应出现一千条平台线程。大量 VT 卡在 synchronized 就是钉住。
六、这里用到了前面哪几条
- 入门:UTF-8 字节长度 ≠
String.length() - 对象:record 换实例,不改字段
- 集合:CHM、
putIfAbsent、replace - 异常:中断恢复标记;try-with-resources 关 exchange
- 并发 / 虚拟线程:per-task,不池,不用 synchronized 包 I/O
- 类加载:有包名;
jdk.httpserver在模块图里 - 网络:阻塞 I/O + VT,不在 handler 里叠 Selector
