فهرست منبع

教程点4:请求头 - HttpRequest层的header/headers/setHeader及Client级默认头变通方案

yangyi 1 هفته پیش
والد
کامیت
bec7b86be5
3فایلهای تغییر یافته به همراه264 افزوده شده و 1 حذف شده
  1. 76 1
      doc.md
  2. 117 0
      src/main/java/space/anyi/httpClient/HeaderExample.java
  3. 71 0
      src/test/java/space/anyi/httpClient/HeaderExampleTest.java

+ 76 - 1
doc.md

@@ -179,4 +179,79 @@ Result<UserVO> result = objectMapper.readValue(response.body(),
 - `createUserAndGetUser_success`:解析出创建后的用户对象,账号与请求一致;
 - `createUser_duplicateAccount`:相同账号重复创建返回 409。
 
-> 测试使用 `System.nanoTime()` 生成带时间戳的唯一账号,避免与历史数据冲突。
+> 测试使用 `System.nanoTime()` 生成带时间戳的唯一账号,避免与历史数据冲突。
+
+---
+
+## 四、请求头
+
+### 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](https://docs.oracle.com/en/java/javase/17/docs/api/java.net.http/java/net/http/HttpClient.Builder.html)。)
+
+因此请求头只能在 **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>`)。
+
+### 4.2 示例代码
+
+见 `HeaderExample.java`,核心代码如下:
+
+```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。

+ 117 - 0
src/main/java/space/anyi/httpClient/HeaderExample.java

@@ -0,0 +1,117 @@
+package space.anyi.httpClient;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+/**
+ * 请求头示例:在 HttpRequest 层配置请求头
+ *
+ * <p><b>重要说明:</b>JDK 的 {@link HttpClient.Builder} 并<b>没有</b>提供
+ * 「设置默认请求头」的方法(可通过 javap 查看 Builder 源码确认)。
+ * 因此在开发中,请求头都是在 HttpRequest 构造阶段设置的。</p>
+ *
+ * <p>本示例覆盖三个 API:</p>
+ * <ul>
+ *     <li>{@code .header(key, value)}:追加一个请求头</li>
+ *     <li>{@code .headers(k1, v1, k2, v2, ...)}:一次追加多组请求头</li>
+ *     <li>{@code .setHeader(key, value)}:覆盖同名请求头</li>
+ * </ul>
+ */
+public class HeaderExample {
+
+    /** 服务基地址常量 */
+    private static final String BASE_URL = "http://localhost:8080";
+
+    /**
+     * 单个请求头:声明响应格式为 JSON。
+     *
+     * @return 已配置 Accept 请求头的请求对象(未发送)
+     */
+    public HttpRequest buildRequestWithSingleHeader() {
+        return HttpRequest.newBuilder()
+                .uri(URI.create(BASE_URL + "/api/users"))
+                .header("Accept", "application/json")   // 告诉服务器客户端期望的响应格式
+                .GET()
+                .build();
+    }
+
+    /**
+     * 多个请求头:一次追加多组 key-value。
+     *
+     * <p>{@code headers()} 接收「key, value, key, value ...」形式的参数,
+     * 个数必须是偶数。</p>
+     */
+    public HttpRequest buildRequestWithMultipleHeaders() {
+        return HttpRequest.newBuilder()
+                .uri(URI.create(BASE_URL + "/api/users"))
+                .headers(
+                        "Content-Type", "application/json",   // 请求体格式
+                        "Accept", "application/json",         // 期望的响应格式
+                        "X-Request-Id", "tutorial-001"        // 自定义跟踪请求头
+                )
+                .GET()
+                .build();
+    }
+
+    /**
+     * 覆盖同名请求头:{@code setHeader()} 会替换已存在的同名头。
+     *
+     * <p>如果先 addHeader 再 setHeader 相同 key,最终只有一个值(即新值)。</p>
+     */
+    public HttpRequest buildRequestWithSetHeader() {
+        return HttpRequest.newBuilder()
+                .uri(URI.create(BASE_URL + "/api/users"))
+                .header("X-Version", "1")     // 先设置 1
+                .setHeader("X-Version", "2")  // 再覆盖为 2
+                .GET()
+                .build();
+    }
+
+    /**
+     * 「Client 级默认请求头」的变通方案。
+     *
+     * <p>既然 HttpClient 本身不能配置默认头,常见的做法是提供一个
+     * 统一的工厂方法,给后续所有通过该方法构造的请求都预置公共请求头,
+     * 达到「同一客户端发出的请求共享默认头」的效果。</p>
+     *
+     * @return 已预置公共请求头(本示例为 Accept 与 User-Agent)的 Builder
+     */
+    public HttpRequest.Builder newBuilderWithDefaultHeaders() {
+        return HttpRequest.newBuilder()
+                // User-Agent 标识客户端身份
+                .header("User-Agent", "JDK-HttpClient-Tutorial/1.0")
+                // Accept 统一声明期望 JSON 响应
+                .header("Accept", "application/json");
+    }
+
+    /**
+     * 发送带请求头的 GET 请求,并打印响应头中的 Content-Type,演示响应头读取。
+     *
+     * @return GET /api/users 的响应对象
+     */
+    public HttpResponse<String> sendRequestWithHeaders() throws IOException, InterruptedException {
+        HttpClient httpClient = HttpClient.newHttpClient();
+
+        // 使用工厂方法拿到预置了默认头的 Builder,再追加个性化头
+        HttpRequest request = newBuilderWithDefaultHeaders()
+                .uri(URI.create(BASE_URL + "/api/users"))
+                .GET()
+                .build();
+
+        HttpResponse<String> response =
+                httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+
+        // 读取响应头:headers().firstValue(name) 返回的 Optional<String>
+        // 本例服务端返回 JSON,Content-Type 通常为 application/json
+        String contentType = response.headers()
+                .firstValue("Content-Type")
+                .orElse("unknown");
+
+        System.out.println("响应头 Content-Type: " + contentType);
+
+        return response;
+    }
+}

+ 71 - 0
src/test/java/space/anyi/httpClient/HeaderExampleTest.java

@@ -0,0 +1,71 @@
+package space.anyi.httpClient;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 请求头示例的测试类
+ */
+class HeaderExampleTest {
+
+    private final HeaderExample headerExample = new HeaderExample();
+
+    @Test
+    void singleHeader() {
+        // .header() 之后,请求对象应带有一个 Accept 请求头
+        HttpRequest request = headerExample.buildRequestWithSingleHeader();
+
+        assertEquals("application/json",
+                request.headers().firstValue("Accept").orElse(""));
+    }
+
+    @Test
+    void multipleHeaders() {
+        // .headers() 一次设置多组请求头,应能逐一读出
+        HttpRequest request = headerExample.buildRequestWithMultipleHeaders();
+
+        assertEquals("application/json",
+                request.headers().firstValue("Content-Type").orElse(""));
+        assertEquals("application/json",
+                request.headers().firstValue("Accept").orElse(""));
+        assertEquals("tutorial-001",
+                request.headers().firstValue("X-Request-Id").orElse(""));
+    }
+
+    @Test
+    void setHeaderOverrides() {
+        // setHeader 会覆盖同名头,最终只有一个值 2,而不是两个值
+        HttpRequest request = headerExample.buildRequestWithSetHeader();
+
+        var values = request.headers().allValues("X-Version");
+        assertEquals(1, values.size());
+        assertEquals("2", values.get(0));
+    }
+
+    @Test
+    void defaultHeaderBuilder() {
+        // 工厂方法预置的 Builder 应带有默认头
+        HttpRequest request = headerExample.newBuilderWithDefaultHeaders()
+                .uri(java.net.URI.create("http://localhost:8080/api/users"))
+                .GET()
+                .build();
+
+        assertEquals("JDK-HttpClient-Tutorial/1.0",
+                request.headers().firstValue("User-Agent").orElse(""));
+        assertEquals("application/json",
+                request.headers().firstValue("Accept").orElse(""));
+    }
+
+    @Test
+    void sendRequestWithHeaders() throws IOException, InterruptedException {
+        // 带 Accept 请求头发送 GET /api/users,应返回 200
+        HttpResponse<String> response = headerExample.sendRequestWithHeaders();
+
+        assertEquals(200, response.statusCode());
+    }
+}