HttpClient

上一章我们用裸 Socket 写了 TCP 通信——能发字节流,但要自己拼 HTTP 报文、解析状态码、处理 Chunked 编码……太累。HTTP 是互联网最常用的应用层协议,Java 当然要有现成的 API。这一章看 Java 11+ 的现代 HttpClient——同步异步一把梭,API 优雅,性能也好。

一、旧 HttpURLConnection 的问题

Java 早期内置的 HTTP 客户端是 HttpURLConnection(Java 1.1,1997 年):

URL url = new URL("https://api.example.com/users");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
int code = conn.getResponseCode();
try (BufferedReader r = new BufferedReader(
        new InputStreamReader(conn.getInputStream()))) {
    String line;
    while ((line = r.readLine()) != null) System.out.println(line);
}

它的毛病:

  1. API 笨重——URLConnection 设计于 1997 年,命名冗长、类型不友好。
  2. 默认阻塞——没有异步支持,高并发要自己开线程池。
  3. HTTP/2 支持差——只能 HTTP/1.1,无法用 HTTP/2 的多路复用。
  4. API 不直观——getResponseCode/getInputStream 散落各处,Builder 模式缺失。
  5. 维护停滞——Oracle 几乎不更新了。

业界都用 Apache HttpClient、OkHttp 等第三方库。Java 9 引入、Java 11 标准化的新 java.net.http.HttpClient 终于给出了官方现代方案。

二、HttpClient API 总览

新 API 的三个核心类:

作用
HttpClient客户端,管连接池、HTTP 版本、超时等
HttpRequest请求,含 URL、方法、Header、Body
HttpResponse<T>响应,含状态码、Header、Body(T 是 body 类型)

设计是 Builder 模式 + 不可变——HttpClient 和 HttpRequest 都是不可变对象,构建后线程安全,可复用。

import java.net.URI;
import java.net.http.*;
import java.time.Duration;

// 1. 创建 HttpClient (可复用, 线程安全)
HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)        // 优先 HTTP/2
    .connectTimeout(Duration.ofSeconds(10))    // 连接超时
    .followRedirects(HttpClient.Redirect.NORMAL)  // 跟随重定向
    .build();

// 2. 构建请求
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.github.com/users/octocat"))
    .timeout(Duration.ofSeconds(10))
    .header("Accept", "application/json")
    .header("User-Agent", "Java HttpClient")
    .GET()    // 可省略, 默认 GET
    .build();

// 3. 发请求 (同步)
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());

System.out.println(resp.statusCode());         // 200
System.out.println(resp.headers().firstValue("Content-Type"));
System.out.println(resp.body());               // JSON 字符串

HttpClient 是重对象(内部有连接池),全局建一个复用——别每次请求都 new。

三、同步请求:send

send 阻塞当前线程直到响应回来:

HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());

第二个参数是 BodyHandler<T>——告诉它怎么处理响应体:

BodyHandler处理方式
ofString()当字符串
ofByteArray()当字节数组
ofFile(Path)直接写文件(适合下载大文件)
ofInputStream()当 InputStream(流式处理)
ofLines()Stream<String>(按行)
discarding()丢弃 body(只关心状态码)

下载大文件用 ofFile 避免内存爆炸:

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://example.com/bigfile.zip"))
    .build();
Path file = Path.of("download.zip");
HttpResponse<Path> resp = client.send(req, HttpResponse.BodyHandlers.ofFile(file));

四、异步请求:sendAsync

sendAsync 立即返回 CompletableFuture<HttpResponse<T>>——不阻塞,回调在响应回来后触发:

CompletableFuture<HttpResponse<String>> future =
    client.sendAsync(req, HttpResponse.BodyHandlers.ofString());

future.thenAccept(resp -> {
    System.out.println("状态码: " + resp.statusCode());
    System.out.println("Body: " + resp.body());
}).exceptionally(e -> {
    System.err.println("请求失败: " + e.getMessage());
    return null;
});

// 主线程继续干别的...
future.join();   // 需要时阻塞等

异步的威力在并发请求——同时发 10 个请求,总耗时≈最慢的那个,而不是 10 个加起来:

List<URI> uris = List.of(uri1, uri2, uri3, /* ... */);

List<CompletableFuture<String>> futures = uris.stream()
    .map(uri -> client.sendAsync(
            HttpRequest.newBuilder(uri).build(),
            HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body))
    .toList();

CompletableFuture<Void> all = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0]));
all.join();   // 等全部完成

List<String> results = futures.stream()
    .map(CompletableFuture::join)
    .toList();

10 个请求并发发,比串行快 10 倍——这是异步的价值。

五、POST/PUT/DELETE 与 Body

发送 Body 用 HttpRequest.BodyPublishers

import static java.net.http.HttpRequest.BodyPublishers.*;

// POST JSON
String json = "{\"name\":\"张三\",\"age\":20}";
HttpRequest postReq = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .POST(ofString(json))           // 字符串 body
    .build();

// POST 表单
String form = "name=张三&age=20";
HttpRequest formReq = HttpRequest.newBuilder()
    .uri(URI.create("https://example.com/login"))
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(ofString(form))
    .build();

// POST 文件 (上传)
HttpRequest uploadReq = HttpRequest.newBuilder()
    .uri(URI.create("https://example.com/upload"))
    .POST(ofFile(Path.of("data.bin")))
    .build();

// PUT
HttpRequest putReq = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users/1"))
    .PUT(ofString(json))
    .build();

// DELETE
HttpRequest delReq = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users/1"))
    .DELETE()
    .build();

BodyPublishers 的几个工厂:

  • ofString(s) —— 字符串。
  • ofString(s, charset) —— 指定编码。
  • ofByteArray(bytes) —— 字节数组。
  • ofFile(path) —— 文件(流式,不占内存)。
  • ofInputStream(supplier) —— 从 InputStream 读。
  • noBody() —— 无 body。

六、超时、Header 与配置

6.1 超时

两层超时:

HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))   // 连接建立超时
    .build();

HttpRequest req = HttpRequest.newBuilder()
    .uri(uri)
    .timeout(Duration.ofSeconds(10))         // 整个请求超时
    .build();

connectTimeout 限制 TCP 握手;timeout 限制整个请求(含读 body)。超时抛 HttpTimeoutException

6.2 Header

HttpRequest req = HttpRequest.newBuilder()
    .uri(uri)
    .header("Accept", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("Content-Type", "application/json; charset=utf-8")
    .build();

// 同名多值用 headers
.headers("X-Custom", "v1", "X-Custom", "v2")

// 或 setHeader 覆盖
.setHeader("User-Agent", "MyApp/1.0")

读响应 Header:

HttpResponse<String> resp = client.send(req, ofString());
Optional<String> ct = resp.headers().firstValue("Content-Type");
List<String> cookies = resp.headers().allValues("Set-Cookie");

6.3 HTTP/2 与重定向

HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)             // 优先 HTTP/2 (默认)
    .followRedirects(HttpClient.Redirect.NORMAL)    // 跟随重定向 (同协议)
    .proxy(ProxySelector.of(new InetSocketAddress("proxy.host", 8080)))  // 代理
    .authenticator(new Authenticator() {            // 认证
        @Override protected PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication("user", "pwd".toCharArray());
        }
    })
    .build();

Redirect 三档:NEVER(不跟随)、NORMAL(跟随同协议)、ALWAYS(跨协议也跟,如 HTTPS→HTTP,有安全风险)。

七、WebSocket

Java 9+ 的 java.net.http.WebSocket 提供原生 WebSocket 客户端:

import java.net.http.WebSocket;
import java.net.URI;

WebSocket ws = HttpClient.newHttpClient()
    .newWebSocketBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .buildAsync(URI.create("wss://echo.example.com/socket"),
        new WebSocket.Listener() {
            @Override public void onOpen(WebSocket webSocket) {
                System.out.println("[WS] 连接打开");
                webSocket.sendText("Hello WebSocket", true);
            }
            @Override public CompletionStage<?> onText(WebSocket webSocket,
                    CharSequence data, boolean last) {
                System.out.println("[WS] 收到: " + data);
                return null;
            }
            @Override public void onError(WebSocket webSocket, Throwable error) {
                System.err.println("[WS] 错误: " + error);
            }
            @Override public CompletionStage<?> onClose(WebSocket webSocket,
                    int statusCode, String reason) {
                System.out.println("[WS] 关闭: " + statusCode + " " + reason);
                return null;
            }
        })
    .join();

ws.sendText("ping", true);
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye");

WebSocket 是双向全双工通信——服务端能主动推消息,适合聊天、实时通知、股票行情。Listener 是回调接口,每个事件对应一个方法。

八、实战:调用 REST API

由于 Piston 在线环境不一定能访问外网,下面代码做了双模式——能联网就调真实 API,不能联网就用本地模拟服务器演示完整 HTTP 流程。

Java · 在线运行

观察重点

  • HttpClient 复用一个实例——内部有连接池,不要每次 new。
  • 同步 send、异步 sendAsync——异步返回 CompletableFuture,可 thenAccept/exceptionally 链式处理。
  • 5 个并发请求总耗时≈单个请求——异步并发是 HttpClient 的杀手锏。
  • 状态码 201/404 都能拿到——通过 statusCode() 判断业务结果。
  • 异常进 exceptionally——超时、连接失败等异常不会进 thenAccept,要单独处理。
  • BodyHandlers.ofString 把 body 当字符串——大文件用 ofFile 流式落盘。

九、HttpClient vs 第三方库

优点缺点
Java HttpClient内置无需依赖、API 现代、HTTP/2功能相对简单
Apache HttpClient功能丰富、生态成熟API 略繁琐、依赖大
OkHttpAPI 优雅、性能好、Android 主流需依赖
Retrofit声明式 REST 客户端(基于 OkHttp)需依赖

选型建议

  • 简单项目、不想引依赖 → Java HttpClient。
  • Spring Boot 项目 → 用 RestTemplate/WebClient(底层就是 HttpClient/Jetty)。
  • 复杂需求(拦截器、复杂重试)→ Apache HttpClient 或 OkHttp。
  • 声明式 REST(Feign 风格)→ Spring Cloud OpenFeign。

十、本章小结

概念核心要点
HttpClient客户端,复用,含连接池
HttpRequest请求,Builder 构建,不可变
HttpResponse<T>响应,T 由 BodyHandler 决定
send同步阻塞
sendAsync异步返回 CompletableFuture
BodyHandlers处理响应体(ofString/ofFile/…)
BodyPublishers生成请求体(ofString/ofFile/…)
WebSocket双向全双工通信,Listener 回调

记忆口诀

  • HttpClient 全局复用——别每次 new,连接池白瞎了。
  • 同步 send、异步 sendAsync——异步返回 CompletableFuture
  • BodyHandler 处理响应——ofString 当字符串,ofFile 落盘。
  • 并发用 allOf——同时发 N 个,总耗时≈最慢的。
  • 超时两层——connectTimeout 连接、timeout 整个请求。

结语:第十一阶段完结

这一章我们用 Java 11+ 的 HttpClient 调用了 REST API——同步异步都有,API 优雅。回头看第十一阶段:

  • 第 59 章 JDBC 详解 —— 数据库连接、PreparedStatement、事务、批处理。
  • 第 60 章 连接池 —— HikariCP/Druid,避免反复建连接。
  • 第 61 章 网络编程 —— Socket/ServerSocket,TCP/UDP 通信。
  • 第 62 章 HttpClient(本章) —— 现代 HTTP 客户端,同步异步。

这四章让你能和”外部世界”打交道——数据库、网络、HTTP。下一阶段我们离开”通信”,转向工程化与生态——Maven/Gradle 构建工具、Git 版本控制、JUnit 测试、日志框架、Lombok——把”能跑的代码”变成”能上线的产品”。