HttpClient / HttpRequest / HttpResponse 源码
概述
JDK 11 引入的 java.net.http 模块提供了标准 HTTP 客户端,替代 HttpURLConnection。它的设计核心是异步优先:所有请求最终都落到 CompletableFuture 与事件驱动的 SelectorManager 上,同步 API 只是异步 API 的 .get() 包装。
实现位于 jdk.internal.net.http 包:HttpClientImpl 组装执行器、Cookie 管理、代理、SSL 与选择器;Exchange 抽象一次请求往返;Http1Exchange / Http2Exchange 分别实现两个协议版本。本文基于 OpenJDK 21 源码拆解这条链路。
核心源码解析
① HttpClient.newHttpClient() 的构建
java
public abstract class HttpClient {
public static HttpClient newHttpClient() {
return new HttpClientBuilderImpl().build(); // 默认配置构建
}
public static Builder newBuilder() { return new HttpClientBuilderImpl(); }
...
}
// jdk.internal.net.http.HttpClientImpl
public final class HttpClientImpl extends HttpClient {
final Executor executor; // 请求执行线程池
final CookieManager cookieManager; // Cookie 管理
final ProxySelector proxySelector; // 代理选择
final SSLContext sslContext; // TLS 上下文
final SelectorManager selectorManager;// IO 事件循环(核心)
...
}newHttpClient()是"零配置"快速入口:内部仍是HttpClientBuilderImpl构建,只是使用默认的executor(Executors.newCachedThreadPool)、无 Cookie 管理、系统默认代理与 TLS。HttpClientImpl的五个组件决定请求全链路:执行器负责用户代码调度,SelectorManager负责所有连接的 IO 事件。- 自定义构建:
HttpClient.newBuilder().connectTimeout(...).followRedirects(NORMAL).proxy(...)等配置都通过HttpClientBuilderImpl逐项注入。
② HttpClient.send(HttpRequest, BodyHandler) 的同步发送
java
// HttpClientImpl
public <T> HttpResponse<T> send(HttpRequest req, HttpResponse.BodyHandler<T> responseHandler)
throws IOException, InterruptedException {
try {
return sendAsync(req, responseHandler).get(); // ① 同步 = 异步阻塞等待
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // 恢复中断标志
throw new IOException("Interrupted", ie);
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) throw (IOException) cause;
...
}
}- 同步是异步的包装:
send()直接委托sendAsync().get(),语义完全一致,只是阻塞调用线程直到响应完成。 - 中断处理:
get()抛InterruptedException时恢复线程中断标志并包装为IOException;CompletionException拆包还原原始异常(如ConnectException)。 - 最终调用
Exchange:sendAsync内部创建Exchange(一次请求-响应的执行上下文),见 ③。
③ HttpClient.sendAsync(HttpRequest, BodyHandler) 的异步发送
java
// HttpClientImpl
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest userRequest,
HttpResponse.BodyHandler<T> responseHandler) {
// ① 请求过滤器(代理、Cookie、认证等)
MultiExchange<T> mex = new MultiExchange<>(userRequest, this, responseHandler, ...);
CompletableFuture<HttpResponse<T>> res = mex.responseAsync();
return res;
}
// Exchange(单次连接上的请求-响应)
final class Exchange<T> implements Cancellable {
final HttpRequest request;
final HttpClientImpl client;
final ExchangeImpl<T> exchangeImpl; // Http1Exchange 或 Http2Exchange
...
CompletableFuture<HttpResponse<T>> responseAsync() {
...
return exchangeImpl.responseAsync(); // 协议实现负责 IO
}
}MultiExchange处理一次逻辑请求可能经历的多次实际交换(重定向、认证质询),Exchange是单次连接上的往返。ExchangeImpl.responseAsync()返回CompletableFuture<HttpResponse<T>>:建立连接、写请求、读响应全链路异步。- 事件驱动:真正的 IO 由
SelectorManager轮询完成(见 ④),用户线程不阻塞在读写上。
④ SelectorManager 的 IO 事件轮询
java
// jdk.internal.net.http.SelectorManager
final class SelectorManager extends Thread {
final Selector selector; // NIO 选择器
final Queue<AsyncEvent> readyEvents; // 已就绪事件队列
final Queue<AsyncEvent> registrations; // 待注册事件队列(跨线程安全)
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// ① 处理注册请求(其他线程提交的 channel 注册)
processPendingRegistrations();
// ② 处理就绪事件:取出 SelectionKey 对应的事件回调
processReadyEvents();
// ③ 阻塞选择,等待 IO 就绪
selector.select(1000);
} catch (IOException x) { ... }
}
}
}- 单线程事件循环:所有连接的注册与就绪分发都在
SelectorManager线程串行完成,避免并发竞争。 - 跨线程安全:其他线程(发起请求的线程)通过
registrations队列提交注册请求,processPendingRegistrations在循环内消费——这是典型的"生产者-消费者"通道。 processReadyEvents把就绪的SelectionKey对应的AsyncEvent(连接建立/可读/可写)回调到ExchangeImpl.handleEvent(),驱动请求状态机前进。
⑤ HttpRequest.BodyPublishers.ofString(String) 的请求体发布
java
public static BodyPublisher ofString(String body) {
return ofString(body, UTF_8);
}
public static BodyPublisher ofString(String body, Charset charset) {
return new StringBodyPublisher(body, charset); // 编码为字节数组
}
// 内部:ByteArrayPublisher
class ByteArrayPublisher implements HttpRequest.BodyPublisher {
private final byte[] content;
...
public Iterator<ByteBuffer> body() {
return List.of(ByteBuffer.wrap(content)).iterator(); // 惰性包装
}
public long contentLength() { return content.length; }
}StringBodyPublisher构造时把字符串按 charset 编码为byte[],请求发送时通过ByteBuffer.wrap包装为缓冲区序列。- 惰性发布:
body()返回的Iterator<ByteBuffer>在真正发送时才消费——内存占用与发送时机解耦。 - 发送落地:
SocketChannel.write(ByteBuffer)(HTTP/1.1 连接)把缓冲区写入网络;contentLength供请求头Content-Length使用。
⑥ HttpResponse.BodyHandlers.ofString() 的响应体收集
java
public static BodyHandler<String> ofString(Charset charset) {
return (statusCode, responseHeaders) -> new StringBodyHandler(charset);
}
// 内部:StringBodyHandler
class StringBodyHandler implements BodySubscriber<String> {
final Charset charset;
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 字节收集
...
public void onNext(List<ByteBuffer> buffers) {
for (ByteBuffer b : buffers)
baos.write(b.array(), b.arrayOffset() + b.position(), b.remaining());
}
public CompletionStage<String> getBody() {
return CompletableFuture.completedFuture(new String(baos.toByteArray(), charset));
}
}BodyHandler是"响应元数据 → BodySubscriber"的工厂:ofString()忽略状态码/头,直接创建字符串收集器。- 收集流程:
onNext把分块到达的ByteBuffer写入ByteArrayOutputStream,完成后getBody()一次性new String(bytes, charset)解码。 - 其他内置处理器同理:
ofByteArray()直接拼字节数组、ofInputStream()包装为流、ofFile()写入文件——都是"收集策略"的差异。
⑦ Http1Exchange vs Http2Exchange
| 维度 | Http1Exchange | Http2Exchange |
|---|---|---|
| 协议 | HTTP/1.1 | HTTP/2 |
| 传输 | PlainHttpConnection(SocketChannel 单连接) | Http2Connection(连接帧多路复用) |
| 请求格式 | 行协议(请求行 + 头部行 + 空行 + 体) | 二进制 HEADERS / DATA 帧 |
| 并发 | 一连接一请求 | 多流(stream)复用同一连接 |
| 状态推进 | ExchangeImpl.responseAsync 逐段读写 | 帧收发 + 流调度 |
Http1Exchange:把请求序列化为 HTTP/1.1 文本行,通过SocketChannel写;读响应时按行解析状态行与头部,再按Content-Length/chunked 读体。Http2Exchange:注册到共享的Http2Connection,用流 id 标识本次请求;HEADERS帧承载请求/响应头,DATA帧承载体,多个流并发复用连接。- 协议选择:默认
Version.HTTP_2,协商失败(服务器不支持)自动降级 HTTP/1.1;HttpClient.newBuilder().version(HTTP_1_1)可强制 1.1。
⑧ CookieManager / CookieStore 的自动管理
java
// CookieFilter(请求过滤链中的一环)
final class CookieFilter implements HeaderFilter {
final HttpClientImpl client;
public void request(HttpRequestImpl r, MultiExchange<?> e) {
CookieStore cookieStore = client.cookieManager().getCookieStore();
List<HttpCookie> cookies = cookieStore.get(r.uri()); // ① 按 URI 取 Cookie
if (cookies != null) {
String cookieHeader = cookies.stream()
.map(c -> c.getName() + "=" + c.getValue())
.collect(joining("; "));
if (!cookieHeader.isEmpty())
r.setSystemHeader("Cookie", cookieHeader); // ② 附加请求头
}
}
}HttpClientImpl的请求过滤链(FilterFactory)包含CookieFilter、ProxyFilter、AuthenticationFilter——每个过滤器在发送前改写请求。CookieManager.getCookieStore().add(uri, cookie)由响应侧(Http1Response/Http2Response处理Set-Cookie头时)自动调用,形成"响应存、请求取"闭环。- 策略:
CookieManager默认AcceptAllCookiePolicy,可按域名/路径过滤;不使用CookieManager时客户端不做任何 Cookie 管理(需自行处理请求头)。
⑨ Redirect 的重定向策略
java
public enum Redirect {
NEVER, // 不跟随,返回 3xx 响应本身
ALWAYS, // 无条件跟随(包括 HTTPS→HTTP 降级)
NORMAL // 仅跟随安全的跳转(详见下)
}MultiExchange检测到 3xx 状态码且配置了跟随策略时,调用followRedirects()构造新请求重新发送(最多 5 次,防循环)。NORMAL语义:只对GET/HEAD请求跟随 301/302/303(且不把https降级到http);ALWAYS无条件跟随(可能引入安全降级风险)。- 302/303 会把请求方法改写为
GET并丢弃请求体;307/308 保留方法与请求体(RFC 7231 语义)。 - 重定向次数耗尽仍返回 3xx 时,
send/sendAsync抛出IOException("too many redirects")。
总结
| 组件 | 职责 | 关键机制 |
|---|---|---|
HttpClientImpl | 客户端装配 | executor/cookie/proxy/ssl/selector 五组件 |
send / sendAsync | 请求入口 | 同步 = 异步 .get(),MultiExchange + Exchange |
SelectorManager | IO 事件循环 | 注册队列 + 就绪队列 + selector.select |
BodyPublishers | 请求体 | ByteBuffer.wrap 惰性发布 |
BodyHandlers | 响应体 | 收集字节 → 解码/落盘 |
Http1/2Exchange | 协议实现 | 行协议 vs 帧多路复用 |
CookieFilter | 请求改写 | CookieStore 按 URI 取并附加头 |
Redirect | 重定向 | NEVER / ALWAYS / NORMAL + 5 次上限 |
HttpClient 的设计哲学是"一次构建,全程异步":连接、写入、读取、解码都挂在 SelectorManager 事件循环上推进,用户侧拿到的是统一的 CompletableFuture。理解 Exchange 与 SelectorManager 的协作,就掌握了 JDK HTTP 客户端的运行骨架。