JDK HTTP Client 完全指南:从入门到实战
前言
Java 11 之前,JDK 自带的 HTTP 客户端只有 HttpURLConnection,又笨又难用,很多人干脆用 Apache HttpClient。Java 11 终于把 HttpClient 内置进了 java.net.http:支持 HTTP/2,同步异步都有,日常够用了。
本文基于 Java 17,围绕一个本地运行的 User Management API(http://localhost:8080)做实战演示。每个知识点配三样东西:文字说明、示例代码、JUnit 测试。
运行示例/测试前,请确保 API 服务已在本机 8080 端口启动。
一、快速入门:请求的构造与响应处理
HttpClient 的用法其实就一句话:构造请求,发出去,处理响应。核心 API 记三个就够了:
| 类 | 职责 |
|---|---|
java.net.http.HttpClient | HTTP 客户端,负责发送请求、管理连接 |
java.net.http.HttpRequest | 请求对象,描述 URI、方法、请求头、请求体 |
java.net.http.HttpResponse<T> | 响应对象,携带状态码、响应头和响应体 |
最小化流程分四步:
- 创建客户端:
HttpClient.newHttpClient()用 JDK 默认配置创建实例; - 构造请求:
HttpRequest.newBuilder().uri(...).GET().build()链式构建; - 发送请求:
client.send(request, BodyHandlers.ofString())同步阻塞发送; - 处理响应:通过
HttpResponse获取statusCode()、body()等。
注意:
send()会抛出IOException(IO 失败)和InterruptedException(线程被中断),需要显式处理或向上抛出。
示例代码
完整的例子在 QuickStart.java:
// 1. 创建 HttpClient:newHttpClient() 使用 JDK 默认的配置创建一个客户端
HttpClient httpClient = HttpClient.newHttpClient();
// 2. 构造请求:HttpRequest.newBuilder() 返回一个 Builder,链式配置请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users")) // 设置请求的目标地址
.GET() // 指定请求方法为 GET
.build(); // 结束构建,返回不可变对象
// 3. 发送请求:send() 同步阻塞,BodyHandlers.ofString() 将响应体转为字符串
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 4. 处理响应
System.out.println("HTTP 状态码: " + response.statusCode());
System.out.println("响应体: " + response.body());
测试验证
见 QuickStartTest.java。测试会真实调用本地 API,运行后控制台打印响应结果。
(测试目标:GET http://localhost:8080/api/users,正常情况下返回 {"code":200,"message":"OK","data":[...]})
二、GET 请求
GET 是最常用的 HTTP 方法,用来从服务器拿数据,不带请求体。用 JDK HTTP Client 发 GET 请求,常见写法有三种:
- 不带参数的 GET:
.uri(url)+.GET(),比如查用户列表; - 带路径参数的 GET:把 id 拼进 URL 路径,比如查单个用户
/api/users/1; - 带查询字符串的 GET:
?key=value跟在 URI 后面,服务端按条件过滤。
| 要点 | 说明 |
|---|---|
.GET() | 显式声明请求方法;省略时默认也是 GET,但显式写出更清晰 |
| 路径参数 | 直接拼在 URL 中,如 /api/users/{id} |
| 查询参数 | 拼在 ? 之后,多个用 & 连接 |
| 无请求体 | GET 请求使用 BodyPublishers.noBody()(默认),无需设置请求体 |
| 响应码 | 200 找到资源;404 资源不存在 |
示例代码
代码都在 GetExample.java 里:
// 不带参数的 GET:查询所有用户
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.GET()
.build();
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 带路径参数的 GET:根据 id 查询单个用户
HttpRequest request2 = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users/" + id))
.GET()
.build();
HttpResponse<String> response2 =
httpClient.send(request2, HttpResponse.BodyHandlers.ofString());
// 带查询字符串的 GET(标准写法演示)
URI uri = URI.create(BASE_URL + "/api/users?account=alice01");
测试验证
见 GetExampleTest.java,测试点包括:
getUsers:GET/api/users返回 200,且响应体含code包装字段;getUserById_notExists:GET/api/users/999999999返回 404;buildRequestWithQuery:验证带?account=alice01查询参数的请求 URI 拼接正确,且 GET 请求体为空。
三、POST / PUT / DELETE 请求
3.1 POST 请求
POST 用来向服务器提交数据、创建资源,和 GET 最大的区别是带请求体。提交 JSON 的标准流程:
- 序列化:把 Java 对象转成 JSON 字符串(教程里用 Jackson 的
ObjectMapper); - 设置请求头:
Content-Type: application/json,告诉服务器请求体格式; - 构造请求体:
BodyPublishers.ofString(json)把字符串包装成请求体; - 声明方法:
.POST(publisher)指定请求方法和请求体; - 处理响应:服务端返回统一包装
Result<UserVO>,用TypeReference反序列化拿到具体数据对象。
| 要点 | 说明 |
|---|---|
.POST(BodyPublishers.ofString(json)) | POST 方法必须携带请求体发布器 |
BodyPublishers.ofString | 将字符串作为请求体;另有 ofInputStream/ofByteArray 等 |
| 状态码 200 | 创建成功(该服务返回 200) |
| 状态码 400 | 请求体格式不正确 / 缺少必填字段 |
| 状态码 409 | 账号已存在,创建冲突 |
注意:为便于演示,本项目引入了 Jackson(
jackson-databind)。JDK HttpClient 本身不关心请求体是 JSON 还是其他格式,序列化的职责由调用方承担。
示例代码
例子在 PostExample.java:
// 1. 使用 Jackson 把对象序列化为 JSON 字符串
String json = objectMapper.writeValueAsString(user);
// 2. 构造 POST 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.header("Content-Type", "application/json") // 声明请求体是 JSON
.POST(BodyPublishers.ofString(json)) // 指定请求体
.build();
// 3. 发送请求
HttpResponse<String> response =
HttpClient.newHttpClient().send(request, BodyHandlers.ofString());
// 4. 反序列化统一包装结构 Result<UserVO>
Result<UserVO> result = objectMapper.readValue(response.body(),
new TypeReference<Result<UserVO>>() { });
配套模型类位于 model/ 包下:UserRequest(请求体)、UserVO(用户响应)、Result<T>(统一包装)。
测试验证
见 PostExampleTest.java,测试点包括:
createUser_success:POST 唯一账号返回 200,响应体含 code 与账号;createUserAndGetUser_success:解析出创建后的用户对象,账号与请求一致;createUser_duplicateAccount:相同账号重复创建返回 409。
测试使用
System.nanoTime()生成带时间戳的唯一账号,避免与历史数据冲突。
3.2 PUT 请求
PUT 用来整体更新已有资源,写法跟 POST 几乎一样:同样带 JSON 请求体,同样用 BodyPublishers.ofString。唯一的区别是把 .POST(...) 换成 .PUT(...),目标地址改成带 id 的 /api/users/{id}。
| 要点 | 说明 |
|---|---|
.PUT(BodyPublishers.ofString(json)) | PUT 快捷方法,必须携带请求体发布器 |
| 路径参数 | id 拼在 URL 中,如 /api/users/1 |
| 状态码 200 | 更新成功 |
| 状态码 404 | 用户不存在 |
| 状态码 409 | 账号与其他用户冲突 |
PUT 与 POST 在 HTTP 语义上的区别:POST 是「创建新资源」,PUT 是「整体替换已有资源」。发送代码唯一的差别就是
.POST(...)/.PUT(...)方法名不同。
示例代码
见 PutDeleteExample.java,核心代码如下:
// PUT 请求:更新 id=1 的用户,请求体为更新后的完整用户信息
String json = objectMapper.writeValueAsString(user);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users/" + id))
.header("Content-Type", "application/json")
.PUT(BodyPublishers.ofString(json)) // 与 POST 唯一的区别
.build();
测试验证
见 PutDeleteExampleTest.java,测试点包括:
updateUser_success/updateUserAndGetUser_success:更新成功返回 200,解析出的账号与请求一致;updateUser_notExists:更新不存在的用户返回 404;buildPutRequest_hasBody:构造的 PUT 请求 method 为 "PUT",且携带请求体。
3.3 DELETE 请求
DELETE 用来删除资源,和 GET 一样不带请求体,把 id 拼进路径即可。Builder 提供了 .DELETE() 快捷方法,内部用的就是 BodyPublishers.noBody()。
| 要点 | 说明 |
|---|---|
.DELETE() | DELETE 快捷方法,不传请求体 |
| 路径参数 | id 拼在 URL 中,如 /api/users/1 |
| 状态码 200 | 删除成功(服务端返回 Result<Void>) |
| 状态码 404 | 用户不存在 |
示例代码
看 PutDeleteExample.java 里的 DELETE 部分:
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users/" + id))
.DELETE() // 不携带请求体
.build();
HttpResponse<String> response =
HttpClient.newHttpClient().send(request, BodyHandlers.ofString());
测试验证
见 PutDeleteExampleTest.java,测试点包括:
deleteUser_success:先创建再删除,返回 200;deleteUser_notExists:删除不存在的用户返回 404;buildDeleteRequest_noBody:构造的 DELETE 请求 method 为 "DELETE",且无请求体(bodyPublisher 为空)。
四、请求头
先澄清一个误区:JDK 的 HttpClient.Builder 没有「设置默认请求头」的方法。(可用 javap --module java.net.http java.net.http.HttpClient$Builder 验证,Builder 只提供 cookieHandler / connectTimeout / executor / followRedirects / priority / proxy / authenticator / sslContext / version 等配置。)
所以请求头只能在 HttpRequest 构造阶段配置。JDK 给了三个 API:
| 方法 | 作用 |
|---|---|
.header(key, value) | 追加一个请求头(同名头可存在多个) |
.headers(k1, v1, k2, v2, ...) | 一次追加多组请求头,参数个数必须为偶数 |
.setHeader(key, value) | 与 header 不同,会覆盖已存在的同名头 |
「Client 级默认头」的变通方案:既然 HttpClient 不能配置默认头,一份通用的做法是提供一个工厂方法,统一返回「已预置公共请求头」的 HttpRequest.Builder,同一个客户端发出的请求都能带上公共头(比如 User-Agent、Accept),集中管理。
响应头读取:response.headers() 返回 HttpHeaders 对象,常用方法:firstValue(name)(返回 Optional<String>)、allValues(name)(返回 List<String>)。
示例代码
见 HeaderExample.java:
// 单个请求头
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.header("Accept", "application/json")
.GET()
.build();
// 多个请求头:key/value 成对出现,个数为偶数
HttpRequest request2 = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.headers(
"Content-Type", "application/json",
"Accept", "application/json",
"X-Request-Id", "tutorial-001"
)
.GET()
.build();
// setHeader 覆盖同名头:最终 X-Version 只有一个值 2
HttpRequest request3 = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.header("X-Version", "1")
.setHeader("X-Version", "2")
.GET()
.build();
// 读取响应头
String contentType = response.headers()
.firstValue("Content-Type")
.orElse("unknown");
工厂方法预置默认请求头的变通方案:
public HttpRequest.Builder newBuilderWithDefaultHeaders() {
return HttpRequest.newBuilder()
.header("User-Agent", "JDK-HttpClient-Tutorial/1.0")
.header("Accept", "application/json");
}
测试验证
见 HeaderExampleTest.java,测试点包括:
singleHeader:.header()设置单个头,可正常读出;multipleHeaders:.headers()一次设置三组头;setHeaderOverrides:.setHeader()覆盖后同名字只有一个值;defaultHeaderBuilder:工厂方法预置的公共头可正常读出;sendRequestWithHeaders:带请求头发送 GET 返回 200。
五、请求体
请求体由 BodyPublisher 描述,通过 POST(BodyPublisher)(PUT/PATCH 也一样)传给请求构造器。JDK 内置了多种实现:
| 发布器 | 说明 | 典型场景 |
|---|---|---|
BodyPublishers.ofString(String) | 字符串请求体 | JSON 字符串提交(最常用) |
BodyPublishers.ofByteArray(byte[]) | 字节数组请求体 | 手工构建的二进制/复杂格式体 |
BodyPublishers.ofInputStream(Supplier<InputStream>) | 输入流请求体 | 大文件流式上传,避免整块载入内存 |
BodyPublishers.ofFile(Path) | 文件请求体 | 直接以文件为请求体 |
BodyPublishers.noBody() | 无请求体 | GET/DELETE 等无体请求 |
multipart/form-data 文件上传:JDK HttpClient 没有内置 multipart 支持,按 RFC 2046 规范手工拼请求体即可,格式是这样:
--boundary\r\n
Content-Disposition: form-data; name="file"; filename="report.txt"\r\n
Content-Type: text/plain\r\n
\r\n
(文件内容)\r\n
--boundary--\r\n
注意:Content-Type 请求头里的 boundary 和请求体里用的是同一个,两边不一致的话服务器就没法正确切分字段。
示例代码
代码见 BodyExample.java:
// ofString:字符串 JSON 请求体
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(json)) // <- 字符串请求体
.build();
// ofInputStream:输入流请求体(懒加载)
ByteArrayInputStream stream =
new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
HttpRequest request2 = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofInputStream(() -> stream))
.build();
// 手工构建 multipart/form-data 请求体(文件上传)
String boundary = "----WebKitFormBoundary" + UUID.randomUUID();
byte[] body = buildMultipartBody(fileName, fileBytes, boundary);
HttpRequest request3 = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/files/upload"))
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(BodyPublishers.ofByteArray(body))
.build();
测试验证
见 BodyExampleTest.java,测试点包括:
createUser_withJsonString:ofString 提交 JSON 创建用户,返回 200;createUser_viaInputStream:ofInputStream 提交 JSON 创建用户,返回 200;uploadFile:上传文本文件,FileVO返回的原始文件名与大小一致;uploadFile_empty:上传空文件返回 400;noBodyRequest:GET 请求对象无 bodyPublisher。
六、文件下载
下载和上传相反:服务器把文件内容按二进制流返回。HttpClient 本身不关心响应体怎么消费,区别全在传入的响应处理器上。常用的有三种:
| 处理器 | 得到的内容 | 适用场景 |
|---|---|---|
BodyHandlers.ofFile(Path) | 直接把响应体写入本地文件 | 大文件下载,不占堆内存 |
BodyHandlers.ofByteArray() | byte[] | 文件较小,想整体读入内存处理 |
BodyHandlers.ofInputStream() | InputStream | 流式读取,边读边处理/转发 |
下载要分两步:
- 先上传拿文件名:调用
POST /api/files/upload得到FileVO,其中的storedFileName就是服务器磁盘上的文件名; - 再按文件名下载:GET
/api/files/download/{storedFileName},服务器把文件内容作为二进制流返回。
响应头里的
Content-Disposition携带attachment标记与原文件名,可通过response.headers().firstValue("Content-Disposition")读取;注意本服务返回的文件名是 storedFileName 去掉扩展名后的部分。
示例代码
见 FileDownloadExample.java,核心代码如下:
// 方式一:ofFile — 直接落盘到本地路径
HttpResponse<Path> resp = httpClient.send(request,
HttpResponse.BodyHandlers.ofFile(target));
// 方式二:ofByteArray — 读入内存得到 byte[]
HttpResponse<byte[]> resp2 = httpClient.send(request,
HttpResponse.BodyHandlers.ofByteArray());
// 方式三:ofInputStream — 以输入流形式消费
HttpResponse<InputStream> resp3 = httpClient.send(request,
HttpResponse.BodyHandlers.ofInputStream());
下载 URL 的构造同 GET:BASE_URL + "/api/files/download/" + storedFileName。
测试验证
见 FileDownloadExampleTest.java,测试点包括:
downloadToFile_success:下载内容写入指定本地文件,内容与服务端一致;downloadAsBytes_success:下载得到 byte[],内容一致;downloadAsStream_success:从 InputStream 读出的内容一致;readContentDisposition_present:响应头含Content-Disposition且带attachment与原文件名;uploadThenDownload_roundTrip:上传后再下载,字节完全一致(round-trip);download_notExists:下载不存在的文件返回 404。
每个测试都先调用上传接口生成临时文件、拿到
storedFileName再下载,结束后清理临时文件。
七、同步与异步
HttpClient 有同步、异步两种发送方式:
- 同步
send():阻塞当前线程直到收到完整响应。
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
- 直接返回
HttpResponse; - 需处理
IOException(网络/IO 失败)与InterruptedException(线程中断); - 适用于请求少的场景,简单直观。
- 异步
sendAsync():立即返回CompletableFuture<HttpResponse>,请求在内部线程池里执行,不阻塞调用线程。
CompletableFuture<HttpResponse<String>> future =
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response = future.join(); // 阻塞式获取,或:
future.thenApply(resp -> ...); // 回调式处理,不阻塞
- 获取结果有两种方式:
join():阻塞直到完成(异常以CompletionException抛出);- 回调链:
thenApply/whenComplete/thenCompose等CompletableFutureAPI;
CompletableFuture.allOf(...)可等待多个并发请求全部完成,非常适合批量并发。
| 对比项 | send() | sendAsync() |
|---|---|---|
| 阻塞 | 阻塞当前线程 | 不阻塞 |
| 返回 | HttpResponse | CompletableFuture<HttpResponse> |
| 异常 | IOException / InterruptedException | CompletionException |
| 适用 | 少量串行请求 | 批量、并发、回调链 |
示例代码
以 SyncAsyncExample.java 为例:
// 同步发送
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 异步发送 + join 阻塞取结果
CompletableFuture<HttpResponse<String>> future =
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> result = future.join();
// 异步发送 + 回调链(不阻塞主线程)
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(resp -> "状态码=" + resp.statusCode()
+ ", body=" + abbreviate(resp.body()));
// 批量并发:并发发起 N 个请求,全部完成后汇总
List<CompletableFuture<HttpResponse<String>>> futures =
IntStream.range(0, count)
.mapToObj(i -> httpClient.sendAsync(buildGetRequest(),
HttpResponse.BodyHandlers.ofString()))
.collect(Collectors.toList());
CompletableFuture<Void> all = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
all.join();
return futures.stream()
.map(f -> f.join().statusCode())
.collect(Collectors.toList());
测试验证
见 SyncAsyncExampleTest.java,测试点包括:
sendSync:同步发送返回 200;sendAsync:异步 + join 返回 200;sendAsyncWithCallback:回调链得到处理结果(以状态码=200,开头);sendAsyncInParallel:并发 10 个请求全部成功(状态码均为 200)。
八、响应处理器
BodyHandler 决定响应体怎么消费:拿到响应头(状态码等)之后,它返回一个 BodySubscriber,后者把响应体的字节流转成目标类型 T。发送时作为第二个参数传入:client.send(request, bodyHandler)。
JDK 内置的响应处理器(BodyHandlers):
| 处理器 | 响应体类型 | 适用场景 |
|---|---|---|
BodyHandlers.ofString() | String | 文本/JSON 响应(最常用) |
BodyHandlers.ofByteArray() | byte[] | 二进制内容 |
BodyHandlers.ofFile(Path) | Path | 大文件下载,直接落盘 |
BodyHandlers.ofInputStream() | InputStream | 流式读取 |
BodyHandlers.discarding() | Void | 只关心状态码,body() 为 null |
BodyHandlers.ofLines() | Stream<String> | 逐行处理 |
自定义 BodyHandler 只需实现 apply(ResponseInfo),返回一个 BodySubscriber 就行:
- 按状态码分流:2xx 正常读取,非 2xx 用
BodySubscribers.replacing(null)丢弃响应体,body() 返回 null; - 响应后处理:用
BodySubscribers.mapping(upstream, fn)把上游订阅器的结果再转一次(比如加个前缀、组装业务对象)。
提示:自定义
BodyHandler<T>通常与BodySubscribers.ofString/ofByteArray组合使用,实现「先读字节流、再按业务逻辑解析」的能力,是接入统一响应包装结构的标准方式。
示例代码
见 ResponseHandlerExample.java,核心代码如下:
// 内置:ofString
HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
// 内置:ofFile(下载直接落盘)
HttpResponse<Path> resp2 = client.send(req, BodyHandlers.ofFile(target));
// 自定义:按状态码分流(2xx 正常读取,非 2xx 丢弃响应体)
BodyHandler<String> handler = responseInfo -> {
if (responseInfo.statusCode() >= 200 && responseInfo.statusCode() < 300) {
return BodySubscribers.ofString(StandardCharsets.UTF_8);
}
return BodySubscribers.replacing(null);
};
// 自定义:响应后处理(加前缀标记)
BodySubscriber<String> upstream = BodySubscribers.ofString(StandardCharsets.UTF_8);
BodyHandler<String> handler2 = responseInfo ->
BodySubscribers.mapping(upstream, body -> "[TAG] " + body);
测试验证
见 ResponseHandlerExampleTest.java,测试点包括:
getAsString/getAsByteArray/getAsFile/getAsInputStream:验证各内置处理器行为;getDiscarded:discarding 处理器 body() 为 null;customHandler_success:自定义 mapping 处理器为响应体加前缀[MY-TAG];customHandler_404DiscardsBody:自定义处理器在 404 时 body() 为 null;statusBasedHandler_successString:按状态码分流的处理器 2xx 正常返回。
九、HTTP Client 配置项
HttpClient.newBuilder() 返回的 Builder 用来配置客户端的通用行为。配置在 build() 之后就不能改了,所以要一次设好。用到的配置项:
| 配置项 | 作用 | 备注 |
|---|---|---|
.version(HTTP_1_1 / HTTP_2) | 协议版本 | |
.connectTimeout(Duration) | 连接建立超时 | 超时抛 ConnectTimeoutException |
.executor(Executor) | 异步请求线程池 | 默认内置线程池 |
.followRedirects(NEVER/ALWAYS/NORMAL) | 重定向策略 | NORMAL 只跟随 GET 等安全方法 |
.proxy(ProxySelector) | 代理选择器 | ProxySelector.NO_PROXY 直连 |
.cookieHandler(CookieHandler) | Cookie 管理器 | 通常搭配 CookieManager |
.authenticator(Authenticator) | 认证器 | Basic Auth 等 |
.sslContext / .sslParameters | TLS 配置 | 仅 HTTPS 生效 |
.priority(int) | HTTP/2 流优先级 | 范围 1~256,仅 HTTP_2 生效 |
示例代码
看 ClientConfigExample.java:
CookieManager cookieManager = new CookieManager();
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1) // 协议版本
.connectTimeout(Duration.ofSeconds(5)) // 连接超时
.executor(Executors.newFixedThreadPool(4)) // 异步线程池
.followRedirects(HttpClient.Redirect.NORMAL) // 重定向策略
.proxy(ProxySelector.getDefault()) // 代理
.cookieHandler(cookieManager) // Cookie 管理
.build();
配置生效后能用 getter 读出来校验,比如 client.version()、client.connectTimeout()、client.followRedirects()。
测试验证
见 ClientConfigExampleTest.java,测试点包括:
buildConfiguredClient_hasExpectedValues:校验 version / connectTimeout / followRedirects / executor / cookieHandler 配置均生效;buildHttp2Client_defaultsToHttp2:HTTP_2 客户端版本正确;sendGet_withConfiguredClient:配置后的客户端请求返回 200;connectTimeout_returns200OrThrows:连接不可达主机时抛出预期异常。
十、HTTP Request 配置项
HttpRequest.newBuilder() 用来构造单个请求。可配项如下,注意它们和 HttpClient 级配置互相独立,请求级的优先级更高:
| 配置项 | 作用 | 备注 |
|---|---|---|
.uri(URI) | 请求地址 | 必填 |
.timeout(Duration) | 整个请求的超时 | 与 client 的 connectTimeout(仅连接阶段)不同 |
.version(HTTP_1_1/HTTP_2) | 请求级协议版本 | 覆盖 client 的 version |
.expectContinue(true) | 期望服务器先返回 100-continue | 标志位存储,发送时才生成 Expect 头 |
.GET()/.POST(pub)/.PUT(pub)/.DELETE() | 标准请求方法 | |
.method(name, publisher) | 自定义请求方法 | PATCH、HEAD 等 |
.header / .headers / .setHeader | 请求头 | |
.copy() | 复制 Builder | 写时复制,修改副本不影响原 Builder |
.timeout 与 .connectTimeout 的区别
| connectTimeout(Client 级) | timeout(Request 级) | |
|---|---|---|
| 作用阶段 | 建立 TCP 连接 | 从发送到拿到完整响应体 |
| 超时抛错 | ConnectTimeoutException | HttpTimeoutException |
示例代码
看 RequestConfigExample.java:
// 请求级超时(整个请求 3 秒)
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.timeout(Duration.ofSeconds(3))
.GET()
.build();
// 期望继续:提交大请求体前先「询价」
HttpRequest request2 = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.header("Content-Type", "application/json")
.expectContinue(true)
.POST(BodyPublishers.ofString(json))
.build();
// 自定义请求方法
HttpRequest request3 = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.method("PATCH", BodyPublishers.noBody())
.build();
// 复制 Builder(写时复制,修改副本不影响原 Builder)
HttpRequest.Builder original = HttpRequest.newBuilder()
.uri(uri).header("X-Original", "yes");
HttpRequest copied = original.copy()
.header("X-Copy", "yes").GET().build();
测试验证
见 RequestConfigExampleTest.java,测试点包括:
timeout_configPresent:请求级 timeout 可读,且为 3 秒;version_override:请求级版本配置生效;expectContinue_flagSet:expectContinue 标志位置为 true(Expect 头在发送阶段生成,headers() 中读取不到);customMethod:method("PATCH", noBody) 生效;copyBuilder_isIndependent:修改副本不影响原 Builder;sendWithTimeout:带请求级超时的请求正常返回 200;timeoutExpired_throwsExpected:不可达地址 + 短超时抛出预期异常。
十一、HTTP Client 核心对象及 API 速览
把前面的示例都过一遍,你会发现核心对象其实只有四个,搞清它们就够用了:
| 核心对象 | 职责 | 获取方式 |
|---|---|---|
HttpClient | 发送请求、管理连接与配置 | HttpClient.newHttpClient() / HttpClient.newBuilder() |
HttpRequest | 描述一次请求(URI、方法、头、体) | HttpRequest.newBuilder().build() |
HttpResponse<T> | 一次请求的结果(状态码、头、体) | client.send(...) 的返回值 |
HttpHeaders | 请求/响应的头部集合 | request.headers() / response.headers() |
HttpClient
newHttpClient()/newBuilder():创建客户端;send(request, bodyHandler)/sendAsync(...):同步/异步发送;version()/connectTimeout()/followRedirects()/executor()/proxy()/cookieHandler():读取创建时配置的各项值。
HttpRequest 与 Builder
- Builder:
.uri()、.header()/headers()/setHeader()、.timeout()、.version()、.expectContinue()、.GET()/.POST()/.PUT()/.DELETE()、.method()、.copy(),最后.build()产出不可变请求; - Request:
.method()、.uri()、.timeout()、.headers()(返回HttpHeaders)、.bodyPublisher()(Optional)。
HttpResponse<T>
.statusCode()、.uri()、.request()、.version();.headers()(返回HttpHeaders)、.body()(目标类型 T);.previousResponse():重定向场景下返回上一次响应。
HttpHeaders
.firstValue(name):返回Optional<String>,取第一个同名值;.allValues(name):返回List<String>,取全部同名值;.map():返回底层Map<String, List<String>>;equals/hashCode/toString等常规方法。
示例代码
参考 CoreApiExample.java:
// 1. 创建客户端
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
System.out.println("version=" + client.version());
System.out.println("connectTimeout=" + client.connectTimeout());
// 2. 构造请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/users"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(3))
.GET()
.build();
// 3. 查看请求对象
System.out.println("method=" + request.method());
HttpHeaders reqHeaders = request.headers();
reqHeaders.firstValue("Accept"); // Optional<String>
request.bodyPublisher().isEmpty(); // true(GET 无体)
// 4. 发送并查看响应对象
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("statusCode=" + response.statusCode());
System.out.println("responseUri=" + response.uri());
// 响应头常用 API
HttpHeaders respHeaders = response.headers();
respHeaders.firstValue("Content-Type").orElse("unknown");
respHeaders.map(); // Map<String, List<String>>
测试验证
见 CoreApiExampleTest.java,测试点包括:
showCoreApis_runsAgainstServer:真实发请求验证所有核心对象 API 可访问;httpRequest_hasExpectedFields:method/uri/headers 读取正确,同名头用allValues返回全部值;httpHeaders_firstAndAllValues:firstValue与allValues行为验证。
总结
JDK HTTP Client 的 API 就那么几件套:HttpClient、HttpRequest、HttpResponse、HttpHeaders,加上 BodyHandlers 和 BodyPublishers 的几个实现。流程上永远是「构建请求 → 发送 → 处理响应」,没有更多花样。
日常开发里我的建议是:
- 简单请求用同步
send(),直观,好调试; - 批量、高并发的场景换
sendAsync()+CompletableFuture,把线程池用起来; - 想统一处理响应(比如都得反序列化
Result<T>),写个自定义BodyHandler,别每次请求都手写一遍; - 所有请求都要带公共头时,用工厂方法返回一个预置好请求头的 Builder。
示例代码在 src/main/java/space/anyi/httpClient/,测试在 src/test/java/space/anyi/httpClient/。跑之前记得先启动 API 服务。
JDK HTTP Client 完全指南:从入门到实战
https://blog.anyi.space/archives/01a07bd7-11be-738c-b607-5457f65c144a
Comments