HttpClient:标准化的 HTTP/1.1 与 HTTP/2 客户端
学完你要能自己回答
- HttpClient 在 JDK 11 是正式 API 吗?它和 JDK 9/10 的 incubator 版本是什么关系?
- send 与 sendAsync 的线程模型、连接池和 HTTP/2 多路复用如何配合?
- 为什么 JDK 11 不能对 HttpClient 使用 try-with-resources?超时、重定向、请求体有哪些边界?
从 incubator 到标准模块01
HttpClient 最早以 incubator 形式出现在 JDK 9(JEP 110),模块名是 jdk.incubator.http,包名是 jdk.incubator.http。JDK 11 的 JEP 321 把它标准化为 java.net.http,API 也做了调整。JDK 10 里它仍然是 incubator,不能用 --release 10 编译 java.net.http。
java --describe-module java.net.http
java.net.http@11.0.28 exports java.net.http requires java.base mandated
模块只导出 java.net.http,实现位于 jdk.internal.net.http,不属于公共 API。不要 import 或依赖内部类;升级 JDK 时它们随时可能变化。
javac --release 10 HttpApi.java
HttpApi.java:1: error: package java.net.http does not exist
import java.net.http.*;
^
HttpApi.java:4: error: cannot find symbol
System.out.println(HttpClient.newHttpClient().version());
^
symbol: variable HttpClient
| 版本 | 状态 | 包名 / 模块 | 说明 |
|---|---|---|---|
| JDK 9 | incubator | jdk.incubator.http | JEP 110,需要 --add-modules |
| JDK 10 | incubator | jdk.incubator.http | API 仍在调整 |
| JDK 11 | 正式 | java.net.http | JEP 321,随 java.se 提供 |
| JDK 21 | 增强 | java.net.http | HttpClient 实现 AutoCloseable,增加 close/shutdown |
三个对象:Client、Request、Response02
HttpClient 持有配置与连接池,HttpRequest 描述一次请求,HttpResponse 由 BodyHandler 决定响应体类型。Client 和 Request 都是不可变对象,可以安全复用。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpGet {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:8080/hello"))
.timeout(Duration.ofSeconds(5))
.header("Accept", "text/plain")
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
System.out.println(response.version());
}
}
这段代码假设本地 8080 端口有一个返回 hello 的服务;本文后面的「连接池」示例会启动一个真实的本地 HttpServer。响应状态、body 和协议版本都由服务端决定。
HttpClient.newHttpClient()使用默认配置;需要超时、代理、认证、重定向策略时用newBuilder()。HttpRequest.newBuilder(URI)必须传 URI;GET()、POST(BodyPublisher)、PUT、DELETE等方法二选一。BodyHandlers.ofString()把响应体解码为字符串;还有ofByteArray、ofFile、ofLines、discarding。response.headers()是大小写不敏感的 HttpHeaders;用firstValue("X-Test")取第一个值。
sendAsync 与 CompletableFuture03
send 会阻塞调用线程直到响应完成;sendAsync 立刻返回 CompletableFuture<HttpResponse<T>>,由客户端内部线程池完成。两者走同一条异步 I/O 管线,区别只在调用方是否等待。
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
CompletableFuture<String> body = future
.thenApply(HttpResponse::body)
.exceptionally(e -> "fallback: " + e.getClass().getSimpleName());
System.out.println(body.get(2, TimeUnit.SECONDS));
返回的 CompletableFuture 在客户端 executor 上完成,不保证在调用线程执行。需要控制后续阶段使用哪个线程池时,用 thenApplyAsync(fn, executor) 显式传入 executor。
| 场景 | API | 异常形态 |
|---|---|---|
| 调用方可以阻塞 | send | 直接抛 IOException / InterruptedException / HttpTimeoutException |
| 调用方不能阻塞 | sendAsync | Future 异常完成;get() 包成 ExecutionException,join() 包成 CompletionException |
| 需要并发多个请求 | sendAsync + allOf | 逐个 future 处理异常,避免一个失败吞掉全部 |
| 需要限流 | Semaphore / 自有 executor | 不要用无限并发压垮下游 |
连接池:复用发生在 TCP 层04
HttpClient 内部维护连接池。HTTP/1.1 使用 keep-alive 复用连接;HTTP/2 在一条连接上多路复用多个 stream。连接池按目标地址、代理和 TLS 配置区分,属于单个 client 实例。
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.concurrent.*;
public class HttpPool {
static final Set<Integer> PORTS = ConcurrentHashMap.newKeySet();
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/", ex -> {
PORTS.add(ex.getRemoteAddress().getPort());
byte[] body = "ok".getBytes(StandardCharsets.UTF_8);
ex.sendResponseHeaders(200, body.length);
try (OutputStream os = ex.getResponseBody()) { os.write(body); }
});
server.start();
String url = "http://127.0.0.1:" + server.getAddress().getPort() + "/";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
client.send(request, HttpResponse.BodyHandlers.ofString());
int before = PORTS.size();
for (int i = 0; i < 5; i++) {
client.send(request, HttpResponse.BodyHandlers.ofString());
}
System.out.println("ports.before=" + before + " after=" + PORTS.size());
server.stop(0);
}
}
ports.before=1 after=1
服务端按远端端口计数连接。预热后连续 5 次请求没有新增端口,说明连接被复用了。这个结果只证明 HTTP/1.1 的 keep-alive 复用;HTTP/2 的多路复用需要服务端支持 ALPN 或 h2c。
| 协议 | 连接使用方式 | 并发上限 | 注意 |
|---|---|---|---|
| HTTP/1.1 | 每个连接同时处理一个请求;池可开多条连接 | 受池大小与并发请求数影响 | 顺序请求优先复用同一条连接 |
| HTTP/2 | 一条连接承载多个 stream | 受服务端 SETTINGS_MAX_CONCURRENT_STREAMS 限制 | 响应 version() 才是实际协议 |
| HTTP/2 cleartext | h2c upgrade 或 prior knowledge | 同上 | 本地 HttpServer 默认不支持,通常会回落到 HTTP/1.1 |
超时、重定向与受限请求头05
HttpClient 的默认值有几个容易记错的点:不跟重定向、没有默认超时、请求版本默认 HTTP/2。服务端不支持 HTTP/2 时会回落到 HTTP/1.1,但响应 version() 会告诉你实际用了什么。
| 配置 | 默认值 | 设置位置 | 失败表现 |
|---|---|---|---|
| connectTimeout | 无 | HttpClient.newBuilder() | 连接阶段超时,抛 HttpConnectTimeoutException |
| 请求 timeout | 无 | HttpRequest.newBuilder().timeout() | 整个交换超时,抛 HttpTimeoutException |
| followRedirects | NEVER | HttpClient.newBuilder() | 3xx 原样返回 |
| version | HTTP_2 | HttpClient.newBuilder().version() | 服务端不支持则回落,response.version() 为准 |
| 代理 | 系统默认 ProxySelector | HttpClient.newBuilder().proxy() | 可选 |
| Cookie | 无 | HttpClient.newBuilder().cookieHandler() | 需要显式设置 CookieManager |
HttpRequest slow = HttpRequest.newBuilder(URI.create(base + "/slow"))
.timeout(Duration.ofMillis(100))
.GET()
.build();
try {
client.send(slow, HttpResponse.BodyHandlers.ofString());
} catch (HttpTimeoutException e) {
System.out.println("timeout=" + e.getClass().getSimpleName());
}
timeout=HttpTimeoutException
重定向默认不跟随。HttpClient.Redirect.NORMAL 会跟随 301/302/303/307/308,但不会把 HTTPS 降到 HTTP;ALWAYS 则连这种降级也跟随。
HttpClient follower = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
有些请求头由客户端管理,直接设置会抛 IllegalArgumentException。JDK 11 的受限头包括 Host、Connection、Content-Length、Expect、Upgrade 等;确有需要时可用系统属性 jdk.httpclient.allowRestrictedHeaders 放开,但不建议。
HttpRequest.newBuilder(uri).header("Host", "example.com").GET().build();
java.lang.IllegalArgumentException
try (HttpClient client = HttpClient.newHttpClient()) {
// ...
}
error: incompatible types: try-with-resources not applicable to variable type
try (HttpClient client = HttpClient.newHttpClient()) {
^
(HttpClient cannot be converted to AutoCloseable)
版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| HttpClient | 11 正式 | JEP 321;9/10 是 jdk.incubator.http。 |
| HttpClient AutoCloseable | 21 起 | JDK 11 没有 close(),不能 try-with-resources。 |
| HTTP/2 | 11 支持 | HTTPS 用 ALPN,明文可用 h2c;实际版本看 response.version()。 |
| WebSocket | 11 支持 | HttpClient.newWebSocketBuilder()。 |
| 默认重定向 | NEVER | 需要显式设置 NORMAL 或 ALWAYS。 |
| 默认请求版本 | HTTP_2 | 服务端不支持时回落,不要假设响应一定是 HTTP/2。 |
| HTTP/3 | 不支持 | JDK 11 没有 HTTP/3 支持。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。