使用 Java 构建 HTTP 服务
用 JDK 内置 HttpServer 构建可测最小 HTTP 服务:路由、状态码与优雅关闭入口。
#howto
#growing
#dev
#java
[!info] related notes
- 所属 MOC:Web 后端 · 学习路线
- 前置概念:异常 · 线程池 · JSON
- 后续概念:HttpClient · Spring Web · 优雅关闭
- 容易混淆:生产仍常选成熟服务器/框架
使用 Java 构建 HTTP 服务
目标
- 启动本机 HTTP 端口
- 实现健康检查与简单业务 GET
- 返回正确状态码
- 能关闭服务器
这个场景为什么出现
先不引入 Spring,也能理解请求-响应、线程与路由。框架是装配加速器;HTTP 语义是底座。
[!abstract] 一句话理解 用
com.sun.net.httpserver.HttpServer(JDK 自带)绑定端口与上下文处理器,完成最小可测服务闭环。
最小可运行示例
场景:订单查询服务提供 /health 与 /orders/{id}
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class MiniOrderServer {
public static void main(String[] args) throws Exception {
Map<String, String> db = Map.of("1", "PAID", "2", "CREATED");
int port = args.length == 0 ? 8080 : Integer.parseInt(args[0]);
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.x.x", port), 0);
server.createContext("/", ex -> {
if (!ex.getRequestMethod().equals("GET")) {
ex.getResponseHeaders().set("Allow", "GET");
write(ex, 405, "method-not-allowed");
return;
}
String path = ex.getRequestURI().getPath();
if (path.equals("/health")) {
write(ex, 200, "ok");
} else if (path.startsWith("/orders/")) {
String id = path.substring("/orders/".length());
String status = db.get(id);
if (status == null) write(ex, 404, "not-found");
else write(ex, 200, status);
} else {
write(ex, 404, "not-found");
}
});
ExecutorService workers = Executors.newFixedThreadPool(4);
server.setExecutor(workers);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { server.stop(1); }
finally { workers.shutdown(); }
}));
server.start();
System.out.println("listening " + server.getAddress().getPort());
// HttpServer 的非 daemon 后台线程保持进程运行;Ctrl+C 触发关闭钩子。
}
static void write(com.sun.net.httpserver.HttpExchange ex, int code, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
try {
ex.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
ex.sendResponseHeaders(code, bytes.length);
try (OutputStream os = ex.getResponseBody()) { os.write(bytes); }
} finally {
ex.close();
}
}
}
javac MiniOrderServer.java
java MiniOrderServer
# 另开终端:curl -i http://127.0.x.x:8080/health
# 在服务终端按 Ctrl+C 关闭
期望:GET /health 返回 200 和 ok;GET /orders/1 返回 200 和 PAID;未知路径/id 返回 404;本例对非 GET 请求统一返回 405 并附 Allow: GET。可用 java MiniOrderServer 0 让系统分配测试端口,以打印值为准。
该 API 位于 JDK 的 jdk.httpserver 模块;命名模块项目需声明 requires jdk.httpserver。本例仅绑定回环地址用于学习,没有完整请求体限制、过载保护或生产级停机协调。
结合场景再看三个关注点
- 状态码是契约。
- 线程池执行请求;newFixedThreadPool 的任务队列无界,固定线程数不代表负载已受控。
- 关闭包含服务器与自建线程池;server.stop(1) 停止接入并最多等待约定延迟后关闭交换,自建 executor 仍需单独关闭。
核心步骤与准确模型
- 选择服务器抽象(JDK/框架);HttpServer context 按路径前缀匹配,精确业务路由应在处理器中进一步核实。
- 定义路由与媒体类型
- 注入业务服务
- 错误映射
- 观测与关闭
工程实践
- 超时、请求体大小限制。
- 统一 JSON 错误体。
- 健康检查与就绪检查分离。
- 再引入 Spring 对照。
本节总结
- 标准路径先跑通 HTTP
- 框架后置
- 状态码/线程/关闭是核心
自测题
- 404 与 500 如何分工?
- 为什么处理器里要限制 body 大小?
参考答案
- 404 资源不存在;500 服务器未处理异常。
- 防 OOM/慢请求攻击。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| HttpServer API | API | JDK 服务器 |