|
@@ -119,7 +119,7 @@ URI uri = URI.create(BASE_URL + "/api/users?account=alice01");
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 三、POST 请求
|
|
|
|
|
|
|
+## 三、POST / PUT / DELETE 请求
|
|
|
|
|
|
|
|
### 3.1 文字说明
|
|
### 3.1 文字说明
|
|
|
|
|
|
|
@@ -181,6 +181,86 @@ Result<UserVO> result = objectMapper.readValue(response.body(),
|
|
|
|
|
|
|
|
> 测试使用 `System.nanoTime()` 生成带时间戳的唯一账号,避免与历史数据冲突。
|
|
> 测试使用 `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`,核心代码如下:
|
|
|
|
|
+
|
|
|
|
|
+```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`,核心代码如下:
|
|
|
|
|
+
|
|
|
|
|
+```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 为空)。
|
|
|
|
|
+
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## 四、请求头
|
|
## 四、请求头
|
|
@@ -332,10 +412,71 @@ HttpRequest request3 = HttpRequest.newBuilder()
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 六、同步与异步
|
|
|
|
|
|
|
+## 六、文件下载
|
|
|
|
|
|
|
|
### 6.1 文字说明
|
|
### 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`,核心代码如下:
|
|
|
|
|
+
|
|
|
|
|
+```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 提供两种发送请求的方式:
|
|
HttpClient 提供两种发送请求的方式:
|
|
|
|
|
|
|
|
**1. 同步 `send()`** — 阻塞当前线程直到收到完整响应:
|
|
**1. 同步 `send()`** — 阻塞当前线程直到收到完整响应:
|
|
@@ -372,7 +513,7 @@ future.thenApply(resp -> ...); // 回调式处理,不阻塞
|
|
|
| 异常 | IOException / InterruptedException | CompletionException |
|
|
| 异常 | IOException / InterruptedException | CompletionException |
|
|
|
| 适用 | 少量串行请求 | 批量、并发、回调链 |
|
|
| 适用 | 少量串行请求 | 批量、并发、回调链 |
|
|
|
|
|
|
|
|
-### 6.2 示例代码
|
|
|
|
|
|
|
+### 7.2 示例代码
|
|
|
|
|
|
|
|
见 `SyncAsyncExample.java`,核心代码如下:
|
|
见 `SyncAsyncExample.java`,核心代码如下:
|
|
|
|
|
|
|
@@ -394,7 +535,7 @@ httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
|
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 6.3 测试代码
|
|
|
|
|
|
|
+### 7.3 测试代码
|
|
|
|
|
|
|
|
见 `SyncAsyncExampleTest.java`,测试点包括:
|
|
见 `SyncAsyncExampleTest.java`,测试点包括:
|
|
|
|
|
|
|
@@ -405,9 +546,9 @@ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 七、响应处理器
|
|
|
|
|
|
|
+## 八、响应处理器
|
|
|
|
|
|
|
|
-### 7.1 文字说明
|
|
|
|
|
|
|
+### 8.1 文字说明
|
|
|
|
|
|
|
|
`BodyHandler` 决定「如何消费响应体」:拿到响应头(状态码等)后,
|
|
`BodyHandler` 决定「如何消费响应体」:拿到响应头(状态码等)后,
|
|
|
它返回一个 `BodySubscriber`,后者把响应体字节流转换为目标类型 T。
|
|
它返回一个 `BodySubscriber`,后者把响应体字节流转换为目标类型 T。
|
|
@@ -436,7 +577,7 @@ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
|
> `ofByteArray` 组合使用,实现「先读字节流、再按业务逻辑解析」的能力,
|
|
> `ofByteArray` 组合使用,实现「先读字节流、再按业务逻辑解析」的能力,
|
|
|
> 是接入统一响应包装结构的标准方式。
|
|
> 是接入统一响应包装结构的标准方式。
|
|
|
|
|
|
|
|
-### 7.2 示例代码
|
|
|
|
|
|
|
+### 8.2 示例代码
|
|
|
|
|
|
|
|
见 `ResponseHandlerExample.java`,核心代码如下:
|
|
见 `ResponseHandlerExample.java`,核心代码如下:
|
|
|
|
|
|
|
@@ -461,7 +602,7 @@ BodyHandler<String> handler2 = responseInfo ->
|
|
|
BodySubscribers.mapping(upstream, body -> "[TAG] " + body);
|
|
BodySubscribers.mapping(upstream, body -> "[TAG] " + body);
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 7.3 测试代码
|
|
|
|
|
|
|
+### 8.3 测试代码
|
|
|
|
|
|
|
|
见 `ResponseHandlerExampleTest.java`,测试点包括:
|
|
见 `ResponseHandlerExampleTest.java`,测试点包括:
|
|
|
|
|
|
|
@@ -474,9 +615,9 @@ BodyHandler<String> handler2 = responseInfo ->
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 八、HTTP Client 配置项
|
|
|
|
|
|
|
+## 九、HTTP Client 配置项
|
|
|
|
|
|
|
|
-### 8.1 文字说明
|
|
|
|
|
|
|
+### 9.1 文字说明
|
|
|
|
|
|
|
|
`HttpClient.newBuilder()` 返回的 Builder 可在创建客户端时配置**通用行为**。
|
|
`HttpClient.newBuilder()` 返回的 Builder 可在创建客户端时配置**通用行为**。
|
|
|
所有配置在客户端创建后**不可修改**,因此应在创建时一次设置好。
|
|
所有配置在客户端创建后**不可修改**,因此应在创建时一次设置好。
|
|
@@ -495,7 +636,7 @@ BodyHandler<String> handler2 = responseInfo ->
|
|
|
| `.sslContext / .sslParameters` | TLS 配置 | 仅 HTTPS 生效 |
|
|
| `.sslContext / .sslParameters` | TLS 配置 | 仅 HTTPS 生效 |
|
|
|
| `.priority(int)` | HTTP/2 流优先级 | 范围 1~256,仅 HTTP_2 生效 |
|
|
| `.priority(int)` | HTTP/2 流优先级 | 范围 1~256,仅 HTTP_2 生效 |
|
|
|
|
|
|
|
|
-### 8.2 示例代码
|
|
|
|
|
|
|
+### 9.2 示例代码
|
|
|
|
|
|
|
|
见 `ClientConfigExample.java`,核心代码如下:
|
|
见 `ClientConfigExample.java`,核心代码如下:
|
|
|
|
|
|
|
@@ -515,7 +656,7 @@ HttpClient client = HttpClient.newBuilder()
|
|
|
配置一旦生效,可通过 getter 读取(如 `client.version()`、
|
|
配置一旦生效,可通过 getter 读取(如 `client.version()`、
|
|
|
`client.connectTimeout()`、`client.followRedirects()` 等),方便校验。
|
|
`client.connectTimeout()`、`client.followRedirects()` 等),方便校验。
|
|
|
|
|
|
|
|
-### 8.3 测试代码
|
|
|
|
|
|
|
+### 9.3 测试代码
|
|
|
|
|
|
|
|
见 `ClientConfigExampleTest.java`,测试点包括:
|
|
见 `ClientConfigExampleTest.java`,测试点包括:
|
|
|
|
|
|
|
@@ -527,9 +668,9 @@ HttpClient client = HttpClient.newBuilder()
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 九、HTTP Request 配置项
|
|
|
|
|
|
|
+## 十、HTTP Request 配置项
|
|
|
|
|
|
|
|
-### 9.1 文字说明
|
|
|
|
|
|
|
+### 10.1 文字说明
|
|
|
|
|
|
|
|
`HttpRequest.newBuilder()` 用于构造单个请求,其可配置项如下
|
|
`HttpRequest.newBuilder()` 用于构造单个请求,其可配置项如下
|
|
|
(与 HttpClient 级配置互相独立,请求级优先级更高):
|
|
(与 HttpClient 级配置互相独立,请求级优先级更高):
|
|
@@ -552,7 +693,7 @@ HttpClient client = HttpClient.newBuilder()
|
|
|
| 作用阶段 | 建立 TCP 连接 | 从发送到拿到完整响应体 |
|
|
| 作用阶段 | 建立 TCP 连接 | 从发送到拿到完整响应体 |
|
|
|
| 超时抛错 | `ConnectTimeoutException` | `HttpTimeoutException` |
|
|
| 超时抛错 | `ConnectTimeoutException` | `HttpTimeoutException` |
|
|
|
|
|
|
|
|
-### 9.2 示例代码
|
|
|
|
|
|
|
+### 10.2 示例代码
|
|
|
|
|
|
|
|
见 `RequestConfigExample.java`,核心代码如下:
|
|
见 `RequestConfigExample.java`,核心代码如下:
|
|
|
|
|
|
|
@@ -583,7 +724,7 @@ HttpRequest.Builder original = HttpRequest.newBuilder().uri(uri).header("X-Origi
|
|
|
HttpRequest copied = original.copy().header("X-Copy", "yes").GET().build();
|
|
HttpRequest copied = original.copy().header("X-Copy", "yes").GET().build();
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 9.3 测试代码
|
|
|
|
|
|
|
+### 10.3 测试代码
|
|
|
|
|
|
|
|
见 `RequestConfigExampleTest.java`,测试点包括:
|
|
见 `RequestConfigExampleTest.java`,测试点包括:
|
|
|
|
|
|
|
@@ -594,4 +735,93 @@ HttpRequest copied = original.copy().header("X-Copy", "yes").GET().build();
|
|
|
- `customMethod`:method("PATCH", noBody) 生效;
|
|
- `customMethod`:method("PATCH", noBody) 生效;
|
|
|
- `copyBuilder_isIndependent`:修改副本不影响原 Builder;
|
|
- `copyBuilder_isIndependent`:修改副本不影响原 Builder;
|
|
|
- `sendWithTimeout`:带请求级超时的请求正常返回 200;
|
|
- `sendWithTimeout`:带请求级超时的请求正常返回 200;
|
|
|
-- `timeoutExpired_throwsExpected`:不可达地址 + 短超时抛出预期异常。
|
|
|
|
|
|
|
+- `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`,核心代码如下:
|
|
|
|
|
+
|
|
|
|
|
+```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_firstAndAllValues`:`firstValue` 与 `allValues` 行为验证。
|