# JDK HTTP Client 使用教程 本文档基于 JDK 自带模块 `java.net.http`(Java 11+,本项目使用 Java 17)编写, 围绕本机运行的 **User Management API**(`http://localhost:8080`)进行实战演示。 每个知识点均包含:示例代码、JUnit 测试、文字说明。 > 运行示例/测试前,请确保 API 服务已在本机 8080 端口启动。 --- ## 一、快速入门:请求的构造 与 响应处理 ### 1.1 文字说明 HTTP Client 的基本使用流程只有一步:**构造请求并发送**。核心 API 有三个: | 类 | 职责 | |----|------| | `java.net.http.HttpClient` | HTTP 客户端,负责发送请求、管理连接 | | `java.net.http.HttpRequest` | 请求对象,描述 URI、方法、请求头、请求体 | | `java.net.http.HttpResponse` | 响应对象,携带状态码、响应头和响应体 | 最小化流程分四步: 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`,核心代码如下: ```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 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`。核心代码如下: ```java // 不带参数的 GET:查询所有用户 HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/api/users")) .GET() .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // 带路径参数的 GET:根据 id 查询单个用户 HttpRequest request2 = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + "/api/users/" + id)) .GET() .build(); HttpResponse 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 请求 ### 3.1 文字说明 POST 用于向服务器**提交数据**(创建资源),与 GET 最大的区别是**携带请求体**。 提交 JSON 的标准流程: 1. **序列化**:把 Java 对象转成 JSON 字符串(教程中使用 Jackson `ObjectMapper`); 2. **设置请求头**:`Content-Type: application/json`,告诉服务器请求体格式; 3. **构造请求体**:`BodyPublishers.ofString(json)` 把字符串包装为请求体; 4. **声明方法**:`.POST(publisher)` 指定请求方法及请求体; 5. **处理响应**:服务端返回统一包装 `Result`,用 `TypeReference` 反序列化拿到具体数据对象。 | 要点 | 说明 | |------|------| | `.POST(BodyPublishers.ofString(json))` | POST 方法必须携带请求体发布器 | | `BodyPublishers.ofString` | 将字符串作为请求体;另有 ofInputStream/ofByteArray 等 | | 状态码 200 | 创建成功(该服务返回 200) | | 状态码 400 | 请求体格式不正确 / 缺少必填字段 | | 状态码 409 | 账号已存在,创建冲突 | > 注意:为便于演示,本项目引入了 Jackson(`jackson-databind`)。 > JDK HttpClient 本身不关心请求体是 JSON 还是其他格式,序列化的职责由调用方承担。 ### 3.2 示例代码 见 `PostExample.java`,核心代码如下: ```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 response = HttpClient.newHttpClient().send(request, BodyHandlers.ofString()); // 4. 反序列化统一包装结构 Result Result result = objectMapper.readValue(response.body(), new TypeReference>() { }); ``` 配套模型类位于 `model/` 包下:`UserRequest`(请求体)、`UserVO`(用户响应)、 `Result`(统一包装)。 ### 3.3 测试代码 见 `PostExampleTest.java`,测试点包括: - `createUser_success`:POST 唯一账号返回 200,响应体含 code 与账号; - `createUserAndGetUser_success`:解析出创建后的用户对象,账号与请求一致; - `createUser_duplicateAccount`:相同账号重复创建返回 409。 > 测试使用 `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`)、`allValues(name)`(返回 `List`)。 ### 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。 --- ## 五、请求体 ### 5.1 文字说明 请求体由 **BodyPublisher** 描述,由 `POST(BodyPublisher)`(或 PUT/PATCH) 传入请求构造器中。JDK 内置了多种 BodyPublisher 实现: | 发布器 | 说明 | 典型场景 | |--------|------|----------| | `BodyPublishers.ofString(String)` | 字符串请求体 | JSON 字符串提交(最常用) | | `BodyPublishers.ofByteArray(byte[])` | 字节数组请求体 | 手工构建的二进制/复杂格式体 | | `BodyPublishers.ofInputStream(Supplier)` | 输入流请求体 | 大文件流式上传,避免整块载入内存 | | `BodyPublishers.ofFile(Path)` | 文件请求体 | 直接以文件为请求体 | | `BodyPublishers.noBody()` | 无请求体 | GET/DELETE 等无体请求 | **multipart/form-data 文件上传**:JDK HttpClient 没有内置 multipart 支持, 按 RFC 2046 规范手工拼接请求体即可,格式如下: ```text --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`,核心代码如下: ```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 文字说明 HttpClient 提供两种发送请求的方式: **1. 同步 `send()`** — 阻塞当前线程直到收到完整响应: ```java HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); ``` - 直接返回 `HttpResponse`; - 需处理 `IOException`(网络/IO 失败)与 `InterruptedException`(线程中断); - 适用于请求少的场景,简单直观。 **2. 异步 `sendAsync()`** — 立即返回 `CompletableFuture`, 请求在 HttpClient 的内部线程池中执行,不阻塞调用线程: ```java CompletableFuture> future = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()); HttpResponse response = future.join(); // 阻塞式获取,或: future.thenApply(resp -> ...); // 回调式处理,不阻塞 ``` - 获取结果有两种方式: - `join()`:阻塞直到完成(异常以 `CompletionException` 抛出); - 回调链:`thenApply` / `whenComplete` / `thenCompose` 等 `CompletableFuture` API; - `CompletableFuture.allOf(...)` 可等待多个并发请求全部完成,非常适合批量并发。 | 对比项 | send() | sendAsync() | |--------|--------|-------------| | 阻塞 | 阻塞当前线程 | 不阻塞 | | 返回 | HttpResponse | CompletableFuture\ | | 异常 | IOException / InterruptedException | CompletionException | | 适用 | 少量串行请求 | 批量、并发、回调链 | ### 6.2 示例代码 见 `SyncAsyncExample.java`,核心代码如下: ```java // 同步发送 HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // 异步发送 + join 阻塞取结果 CompletableFuture> future = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()); HttpResponse result = future.join(); // 异步发送 + 回调链(不阻塞主线程) httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(resp -> "状态码=" + resp.statusCode()); // 批量并发:并发发起 N 个请求,全部完成后汇总 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); ``` ### 6.3 测试代码 见 `SyncAsyncExampleTest.java`,测试点包括: - `sendSync`:同步发送返回 200; - `sendAsync`:异步 + join 返回 200; - `sendAsyncWithCallback`:回调链得到处理结果; - `sendAsyncInParallel`:并发 10 个请求全部成功。