doc.md 32 KB

JDK HTTP Client 使用教程

本文档基于 JDK 自带模块 java.net.http(Java 11+,本项目使用 Java 17)编写, 围绕本机运行的 User Management APIhttp://localhost:8080)进行实战演示。 每个知识点均包含:示例代码、JUnit 测试、文字说明。

运行示例/测试前,请确保 API 服务已在本机 8080 端口启动。


一、快速入门:请求的构造 与 响应处理

1.1 文字说明

HTTP Client 的基本使用流程只有一步:构造请求并发送。核心 API 有三个:

职责
java.net.http.HttpClient HTTP 客户端,负责发送请求、管理连接
java.net.http.HttpRequest 请求对象,描述 URI、方法、请求头、请求体
java.net.http.HttpResponse<T> 响应对象,携带状态码、响应头和响应体

最小化流程分四步:

  1. 创建客户端HttpClient.newHttpClient() 用 JDK 默认配置创建实例;
  2. 构造请求HttpRequest.newBuilder().uri(...).GET().build() 链式构建;
  3. 发送请求client.send(request, BodyHandlers.ofString()) 同步阻塞发送;
  4. 处理响应:通过 HttpResponse 获取 statusCode()body() 等。

注意:send() 会抛出 IOException(IO 失败)和 InterruptedException(线程被中断), 需要显式处理或向上抛出。

1.2 示例代码

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());

1.3 测试代码

QuickStartTest.java。测试会真实调用本地 API,运行后控制台打印响应结果。 (测试目标:GET http://localhost:8080/api/users,正常情况下返回 {"code":200,"message":"OK","data":[...]}


二、GET 请求

2.1 文字说明

GET 是最常用的 HTTP 方法,用于从服务器获取数据,且不携带请求体。 用 JDK HTTP Client 发送 GET 请求时,有三种常见写法:

  1. 不带参数的 GET.uri(url) + .GET(),例如查询用户列表;
  2. 带路径参数的 GET:把 id 等参数拼进 URL 路径,例如查询单个用户 /api/users/1
  3. 带查询字符串的 GET?key=value 跟在 URI 后面,服务端按查询条件过滤。

要点总结:

要点 说明
.GET() 显式声明请求方法;省略时默认也是 GET,但显式写出更清晰
路径参数 直接拼在 URL 中,如 /api/users/{id}
查询参数 拼在 ? 之后,多个用 & 连接
无请求体 GET 请求使用 BodyPublishers.noBody()(默认),无需设置请求体
响应码 200 找到资源;404 资源不存在

2.2 示例代码

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");

2.3 测试代码

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 用于向服务器提交数据(创建资源),与 GET 最大的区别是携带请求体。 提交 JSON 的标准流程:

  1. 序列化:把 Java 对象转成 JSON 字符串(教程中使用 Jackson ObjectMapper);
  2. 设置请求头Content-Type: application/json,告诉服务器请求体格式;
  3. 构造请求体BodyPublishers.ofString(json) 把字符串包装为请求体;
  4. 声明方法.POST(publisher) 指定请求方法及请求体;
  5. 处理响应:服务端返回统一包装 Result<UserVO>,用 TypeReference 反序列化拿到具体数据对象。
要点 说明
.POST(BodyPublishers.ofString(json)) POST 方法必须携带请求体发布器
BodyPublishers.ofString 将字符串作为请求体;另有 ofInputStream/ofByteArray 等
状态码 200 创建成功(该服务返回 200)
状态码 400 请求体格式不正确 / 缺少必填字段
状态码 409 账号已存在,创建冲突

注意:为便于演示,本项目引入了 Jackson(jackson-databind)。 JDK HttpClient 本身不关心请求体是 JSON 还是其他格式,序列化的职责由调用方承担。

3.2 示例代码

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>(统一包装)。

3.3 测试代码

PostExampleTest.java,测试点包括:

  • createUser_success:POST 唯一账号返回 200,响应体含 code 与账号;
  • createUserAndGetUser_success:解析出创建后的用户对象,账号与请求一致;
  • createUser_duplicateAccount:相同账号重复创建返回 409。

测试使用 System.nanoTime() 生成带时间戳的唯一账号,避免与历史数据冲突。

3.4 PUT 请求

3.4.1 文字说明

PUT 用于整体更新已有资源,与 POST 的写法几乎一致:同样携带 JSON 请求体、 同样用 BodyPublishers.ofString,唯一区别是把 .POST(...) 换成 .PUT(...), 并把目标地址改为带路径参数的 /api/users/{id}

要点 说明
.PUT(BodyPublishers.ofString(json)) PUT 快捷方法,必须携带请求体发布器
路径参数 id 拼在 URL 中,如 /api/users/1
状态码 200 更新成功
状态码 404 用户不存在
状态码 409 账号与其他用户冲突

PUT 与 POST 在 HTTP 语义上的区别:POST 是「创建新资源」,PUT 是「整体替换 已有资源」。发送代码唯一的差别就是 .POST(...) / .PUT(...) 方法名不同。

3.4.2 示例代码

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();

3.4.3 测试代码

PutDeleteExampleTest.java,测试点包括:

  • updateUser_success / updateUserAndGetUser_success:更新成功返回 200, 解析出的账号与请求一致;
  • updateUser_notExists:更新不存在的用户返回 404;
  • buildPutRequest_hasBody:构造的 PUT 请求 method 为 "PUT",且携带请求体。

3.5 DELETE 请求

3.5.1 文字说明

DELETE 用于删除资源,与 GET 一样不携带请求体,只需把 id 拼进路径。 Builder 提供 .DELETE() 快捷方法(内部天然使用 BodyPublishers.noBody())。

要点 说明
.DELETE() DELETE 快捷方法,不传请求体
路径参数 id 拼在 URL 中,如 /api/users/1
状态码 200 删除成功(服务端返回 Result<Void>)
状态码 404 用户不存在

3.5.2 示例代码

PutDeleteExample.java,核心代码如下:

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(BASE_URL + "/api/users/" + id))
        .DELETE()                            // 不携带请求体
        .build();

HttpResponse<String> response =
        HttpClient.newHttpClient().send(request, BodyHandlers.ofString());

3.5.3 测试代码

PutDeleteExampleTest.java,测试点包括:

  • deleteUser_success:先创建再删除,返回 200;
  • deleteUser_notExists:删除不存在的用户返回 404;
  • buildDeleteRequest_noBody:构造的 DELETE 请求 method 为 "DELETE", 且无请求体(bodyPublisher 为空)。

四、请求头

4.1 文字说明

先澄清一个误区: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-AgentAccept),达到集中管理的效果。

响应头读取response.headers() 返回 HttpHeaders 对象,常用方法: firstValue(name)(返回 Optional<String>)、allValues(name)(返回 List<String>)。

4.2 示例代码

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");

4.3 测试代码

HeaderExampleTest.java,测试点包括:

  • singleHeader.header() 设置单个头,可正常读出;
  • multipleHeaders.headers() 一次设置三组头;
  • setHeaderOverrides.setHeader() 覆盖后同名字只有一个值;
  • defaultHeaderBuilder:工厂方法预置的公共头可正常读出;
  • sendRequestWithHeaders:带请求头发送 GET 返回 200。

五、请求体

5.1 文字说明

请求体由 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 完全一致,服务器才能正确切分字段。

5.2 示例代码

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();

5.3 测试代码

BodyExampleTest.java,测试点包括:

  • createUser_withJsonString:ofString 提交 JSON 创建用户,返回 200;
  • createUser_viaInputStream:ofInputStream 提交 JSON 创建用户,返回 200;
  • uploadFile:上传文本文件,FileVO 返回的原始文件名与大小一致;
  • uploadFile_empty:上传空文件返回 400;
  • noBodyRequest:GET 请求对象无 bodyPublisher。

六、文件下载

6.1 文字说明

下载与上传相反,服务端以二进制流返回文件内容。JDK HTTP Client 本身 对「如何消费响应体」并不关心,区别只在于传入的响应处理器。常用的三种:

处理器 得到的内容 适用场景
BodyHandlers.ofFile(Path) 直接把响应体写入本地文件 大文件下载,不占堆内存
BodyHandlers.ofByteArray() byte[] 文件较小,想整体读入内存处理
BodyHandlers.ofInputStream() InputStream 流式读取,边读边处理/转发

下载流程分两步:

  1. 先上传拿文件名:调用 POST /api/files/upload 得到 FileVO, 其中的 storedFileName 是服务端磁盘上的文件名;
  2. 再按文件名下载:GET /api/files/download/{storedFileName}, 服务端返回文件内容二进制流。

响应头里的 Content-Disposition 携带 attachment 标记与原文件名, 可通过 response.headers().firstValue("Content-Disposition") 读取; 注意本服务返回的文件名是 storedFileName 去掉扩展名后的部分。

6.2 示例代码

FileDownloadExample.java,核心代码如下:

// 方式一:DownloadToFile — 直接落盘到本地路径
HttpResponse<Path> resp = httpClient.send(request,
        HttpResponse.BodyHandlers.ofFile(target));

// 方式二:DownloadAsBytes — 读入内存得到 byte[]
HttpResponse<byte[]> resp2 = httpClient.send(request,
        HttpResponse.BodyHandlers.ofByteArray());

// 方式三:DownloadAsStream — 以输入流形式消费
HttpResponse<InputStream> resp3 = httpClient.send(request,
        HttpResponse.BodyHandlers.ofInputStream());

下载 URL 的构造同 GET:BASE_URL + "/api/files/download/" + storedFileName

6.3 测试代码

FileDownloadExampleTest.java,测试点包括:

  • downloadToFile_success:下载内容写入指定本地文件,内容与服务端一致;
  • downloadAsBytes_success:下载得到 byte[],内容一致;
  • downloadAsStream_success:从 InputStream 读出的内容一致;
  • readContentDisposition_present:响应头含 Content-Disposition 且带 attachment 与原文件名;
  • uploadThenDownload_roundTrip:上传后再下载,字节完全一致(round-trip);
  • download_notExists:下载不存在的文件返回 404。

每个测试都先调用上传接口生成临时文件、拿到 storedFileName 再下载, 结束后清理临时文件。


七、同步与异步

7.1 文字说明

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 / thenComposeCompletableFuture API;
  • CompletableFuture.allOf(...) 可等待多个并发请求全部完成,非常适合批量并发。
对比项 send() sendAsync()
阻塞 阻塞当前线程 不阻塞
返回 HttpResponse CompletableFuture<HttpResponse>
异常 IOException / InterruptedException CompletionException
适用 少量串行请求 批量、并发、回调链

7.2 示例代码

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();

7.3 测试代码

SyncAsyncExampleTest.java,测试点包括:

  • sendSync:同步发送返回 200;
  • sendAsync:异步 + join 返回 200;
  • sendAsyncWithCallback:回调链得到处理结果;
  • sendAsyncInParallel:并发 10 个请求全部成功。

八、响应处理器

8.1 文字说明

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 组合使用,实现「先读字节流、再按业务逻辑解析」的能力, 是接入统一响应包装结构的标准方式。

8.2 示例代码

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);

8.3 测试代码

ResponseHandlerExampleTest.java,测试点包括:

  • getAsString / getAsByteArray / getAsFile / getAsInputStream: 验证各内置处理器行为;
  • getDiscarded:discarding 处理器 body() 为 null;
  • customHandler_success:自定义 mapping 处理器为响应体加前缀;
  • customHandler_404DiscardsBody:自定义处理器在 404 时 body() 为 null;
  • statusBasedHandler_successString:按状态码分流的处理器 2xx 正常返回。

九、HTTP Client 配置项

9.1 文字说明

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 生效

9.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() 等),方便校验。

9.3 测试代码

ClientConfigExampleTest.java,测试点包括:

  • buildConfiguredClient_hasExpectedValues:校验 version / connectTimeout / followRedirects / executor / cookieHandler 配置均生效;
  • buildHttp2Client_defaultsToHttp2:HTTP_2 客户端版本正确;
  • sendGet_withConfiguredClient:配置后的客户端请求返回 200;
  • connectTimeout_returns200OrThrows:连接不可达主机时抛出预期异常。

十、HTTP Request 配置项

10.1 文字说明

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

10.2 示例代码

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();

10.3 测试代码

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

11.1 文字说明

学完以上所有示例后会发现,JDK HTTP Client 的核心对象其实只有四个, 理解了它们就掌握了全部用法。下面作一次集中梳理:

核心对象 职责 获取方式
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 等常规方法。

11.2 示例代码

CoreApiExample.java,核心代码如下:

// 1. 创建客户端
HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build();
System.out.println("version=" + client.version());         // HTTP_2
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();          // -> HttpHeaders
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>>

11.3 测试代码

CoreApiExampleTest.java,测试点包括:

  • showCoreApis_runsAgainstServer:真实发请求验证所有核心对象 API 可访问;
  • httpRequest_hasExpectedFields:method/uri/headers 读取正确, 同名头用 allValues 返回全部值;
  • httpHeaders_firstAndAllValuesfirstValueallValues 行为验证。