本文档基于 JDK 自带模块 java.net.http(Java 11+,本项目使用 Java 17)编写,
围绕本机运行的 User Management API(http://localhost:8080)进行实战演示。
每个知识点均包含:示例代码、JUnit 测试、文字说明。
运行示例/测试前,请确保 API 服务已在本机 8080 端口启动。
HTTP Client 的基本使用流程只有一步:构造请求并发送。核心 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 是最常用的 HTTP 方法,用于从服务器获取数据,且不携带请求体。 用 JDK HTTP Client 发送 GET 请求时,有三种常见写法:
.uri(url) + .GET(),例如查询用户列表;/api/users/1;?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 用于向服务器提交数据(创建资源),与 GET 最大的区别是携带请求体。 提交 JSON 的标准流程:
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()生成带时间戳的唯一账号,避免与历史数据冲突。
先澄清一个误区:JDK 的 HttpClient.Builder 没有「设置默认请求头」的方法。
(可通过 javap --module java.net.http java.net.http.HttpClient$Builder 查看,
Builder 只提供 cookieHandler / connectTimeout / executor / followRedirects /
priority / proxy / authenticator / sslContext / version 等配置。见
官方 API。)
因此请求头只能在 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");
见 HeaderExampleTest.java,测试点包括:
singleHeader:.header() 设置单个头,可正常读出;multipleHeaders:.headers() 一次设置三组头;setHeaderOverrides:.setHeader() 覆盖后同名字只有一个值;defaultHeaderBuilder:工厂方法预置的公共头可正常读出;sendRequestWithHeaders:带请求头发送 GET 返回 200。请求体由 BodyPublisher 描述,由 POST(BodyPublisher)(或 PUT/PATCH)
传入请求构造器中。JDK 内置了多种 BodyPublisher 实现:
| 发布器 | 说明 | 典型场景 |
|---|---|---|
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 必须与请求体中使用的
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 提供两种发送请求的方式:
1. 同步 send() — 阻塞当前线程直到收到完整响应:
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
HttpResponse;IOException(网络/IO 失败)与 InterruptedException(线程中断);2. 异步 sendAsync() — 立即返回 CompletableFuture<HttpResponse>,
请求在 HttpClient 的内部线程池中执行,不阻塞调用线程:
CompletableFuture<HttpResponse<String>> future =
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response = future.join(); // 阻塞式获取,或:
future.thenApply(resp -> ...); // 回调式处理,不阻塞
join():阻塞直到完成(异常以 CompletionException 抛出);thenApply / whenComplete / thenCompose 等 CompletableFuture API;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());
// 批量并发:并发发起 N 个请求,全部完成后汇总
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
见 SyncAsyncExampleTest.java,测试点包括:
sendSync:同步发送返回 200;sendAsync:异步 + join 返回 200;sendAsyncWithCallback:回调链得到处理结果;sendAsyncInParallel:并发 10 个请求全部成功。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:
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));
// 自定义:按状态码分流
BodyHandler<String> handler = responseInfo -> {
if (responseInfo.statusCode() >= 200 && responseInfo.statusCode() < 300) {
return BodySubscribers.ofString(StandardCharsets.UTF_8);
}
return BodySubscribers.replacing(null); // 非 2xx 丢弃响应体
};
// 自定义:响应后处理(加前缀标记)
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 处理器为响应体加前缀;customHandler_404DiscardsBody:自定义处理器在 404 时 body() 为 null;statusBasedHandler_successString:按状态码分流的处理器 2xx 正常返回。HttpClient.newBuilder() 返回的 Builder 可在创建客户端时配置通用行为。
所有配置在客户端创建后不可修改,因此应在创建时一次设置好。
常用配置项:
| 配置项 | 作用 | 备注 |
|---|---|---|
.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:连接不可达主机时抛出预期异常。