JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · NETWORK · JEP 321 (JDK 11)

HttpClient:标准化的 HTTP/1.1 与 HTTP/2 客户端

JDK 11 · 速查主题 02 / 06 · 预计 28 分钟
核心28 分钟JEP 321
一句话java.net.http.HttpClient 在 JDK 11 转正,替代老旧的 HttpURLConnection:同步/异步、HTTP/2、WebSocket、连接池和 BodyHandler 都在一套 API 里。它是线程安全、可复用的长生命周期对象;JDK 11 还没有 close()。

学完你要能自己回答

  1. HttpClient 在 JDK 11 是正式 API 吗?它和 JDK 9/10 的 incubator 版本是什么关系?
  2. send 与 sendAsync 的线程模型、连接池和 HTTP/2 多路复用如何配合?
  3. 为什么 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 9incubatorjdk.incubator.httpJEP 110,需要 --add-modules
JDK 10incubatorjdk.incubator.httpAPI 仍在调整
JDK 11正式java.net.httpJEP 321,随 java.se 提供
JDK 21增强java.net.httpHttpClient 实现 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") 取第一个值。
HttpClient 应该作为长生命周期对象复用。每个请求 new 一个 client 会丢失连接池,还会增加线程与选择器开销。

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
调用方不能阻塞sendAsyncFuture 异常完成;get() 包成 ExecutionException,join() 包成 CompletionException
需要并发多个请求sendAsync + allOf逐个 future 处理异常,避免一个失败吞掉全部
需要限流Semaphore / 自有 executor不要用无限并发压垮下游
JDK 11 没有虚拟线程。想让阻塞式代码获得高并发,要么用 sendAsync,要么把 send 放进有界线程池;不要为每个请求创建一个平台线程。

连接池:复用发生在 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 cleartexth2c upgrade 或 prior knowledge同上本地 HttpServer 默认不支持,通常会回落到 HTTP/1.1
JDK 11 的 HttpClient 没有 close(),也没有实现 AutoCloseable。连接的生命周期跟随 client 实例;进程退出或连接空闲超时后才会释放。需要显式关闭是 JDK 21 才补上的能力。

超时、重定向与受限请求头05

HttpClient 的默认值有几个容易记错的点:不跟重定向、没有默认超时、请求版本默认 HTTP/2。服务端不支持 HTTP/2 时会回落到 HTTP/1.1,但响应 version() 会告诉你实际用了什么。

配置默认值设置位置失败表现
connectTimeout无HttpClient.newBuilder()连接阶段超时,抛 HttpConnectTimeoutException
请求 timeout无HttpRequest.newBuilder().timeout()整个交换超时,抛 HttpTimeoutException
followRedirectsNEVERHttpClient.newBuilder()3xx 原样返回
versionHTTP_2HttpClient.newBuilder().version()服务端不支持则回落,response.version() 为准
代理系统默认 ProxySelectorHttpClient.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)
网上大量 HttpClient 示例是按 JDK 21 写的,会直接使用 try-with-resources。把这类代码放到 JDK 11 上会编译失败;这是版本边界,不是配置问题。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
HttpClient11 正式JEP 321;9/10 是 jdk.incubator.http。
HttpClient AutoCloseable21 起JDK 11 没有 close(),不能 try-with-resources。
HTTP/211 支持HTTPS 用 ALPN,明文可用 h2c;实际版本看 response.version()。
WebSocket11 支持HttpClient.newWebSocketBuilder()。
默认重定向NEVER需要显式设置 NORMAL 或 ALWAYS。
默认请求版本HTTP_2服务端不支持时回落,不要假设响应一定是 HTTP/2。
HTTP/3不支持JDK 11 没有 HTTP/3 支持。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. HttpClient 在 JDK 11 是正式 API 吗?
是。JEP 321 在 JDK 11 把 java.net.http 标准化;JDK 9/10 的 jdk.incubator.http 是孵化版本,包名和 API 都不同。
02. send 和 sendAsync 的底层管线有什么区别?
没有本质区别,都是同一套异步 I/O 与连接池;send 只是同步等待结果,sendAsync 返回 CompletableFuture 由调用方组合。send 不会为每个请求新建线程。
03. 为什么 JDK 11 不能对 HttpClient 使用 try-with-resources?
JDK 11 的 HttpClient 没有实现 AutoCloseable,也没有 close();这个能力是 JDK 21 才加的。JDK 11 上写 try-with-resources 会编译失败。
04. 请求设置 HTTP_2,响应一定是 HTTP/2 吗?
不一定。客户端会尝试 HTTP/2,服务端不支持时回落到 HTTP/1.1;必须以 response.version() 为准。本地 HttpServer 默认只支持 HTTP/1.1。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。