ソースを参照

blog.md:去AI化改写,移除模板化结构与机械化表达

yangyi 1 週間 前
コミット
1b5e215b3c
1 ファイル変更63 行追加81 行削除
  1. 63 81
      blog.md

+ 63 - 81
blog.md

@@ -2,9 +2,9 @@
 
 ## 前言
 
-Java 11 开始,JDK 终于内置了官方的 HTTP 客户端——`java.net.http.HttpClient`。在此之前,发送 HTTP 请求要么依赖 Apache HttpClient,要么使用 HttpURLConnection 这个"上古 API"。新 API 设计现代、支持 HTTP/2、提供同步和异步两种模式,是 Java 生态中处理 HTTP 通信的首选方案
+Java 11 之前,JDK 自带的 HTTP 客户端只有 HttpURLConnection,又笨又难用,很多人干脆用 Apache HttpClient。Java 11 终于把 HttpClient 内置进了 `java.net.http`:支持 HTTP/2,同步异步都有,日常够用了
 
-本文基于 **Java 17**,围绕一个本地运行的 **User Management API**(`http://localhost:8080`)进行实战演示,每个知识点均包含:**文字说明、示例代码、JUnit 测试**
+本文基于 Java 17,围绕一个本地运行的 User Management API(`http://localhost:8080`)做实战演示。每个知识点配三样东西:文字说明、示例代码、JUnit 测试
 
 > 运行示例/测试前,请确保 API 服务已在本机 8080 端口启动。
 
@@ -12,9 +12,7 @@ Java 11 开始,JDK 终于内置了官方的 HTTP 客户端——`java.net.http
 
 ## 一、快速入门:请求的构造与响应处理
 
-### 核心概念
-
-HTTP Client 的基本使用流程只有一句话:**构造请求,发送,处理响应**。核心 API 有三个:
+HttpClient 的用法其实就一句话:构造请求,发出去,处理响应。核心 API 记三个就够了:
 
 | 类 | 职责 |
 |----|------|
@@ -24,16 +22,16 @@ HTTP Client 的基本使用流程只有一句话:**构造请求,发送,处
 
 最小化流程分四步:
 
-1. **创建客户端**:`HttpClient.newHttpClient()` 用 JDK 默认配置创建实例;
-2. **构造请求**:`HttpRequest.newBuilder().uri(...).GET().build()` 链式构建;
-3. **发送请求**:`client.send(request, BodyHandlers.ofString())` 同步阻塞发送;
-4. **处理响应**:通过 `HttpResponse` 获取 `statusCode()`、`body()` 等。
+1. 创建客户端:`HttpClient.newHttpClient()` 用 JDK 默认配置创建实例;
+2. 构造请求:`HttpRequest.newBuilder().uri(...).GET().build()` 链式构建;
+3. 发送请求:`client.send(request, BodyHandlers.ofString())` 同步阻塞发送;
+4. 处理响应:通过 `HttpResponse` 获取 `statusCode()`、`body()` 等。
 
 > 注意:`send()` 会抛出 `IOException`(IO 失败)和 `InterruptedException`(线程被中断),需要显式处理或向上抛出。
 
 ### 示例代码
 
-见 `QuickStart.java`,核心代码如下
+完整的例子在 `QuickStart.java`
 
 ```java
 // 1. 创建 HttpClient:newHttpClient() 使用 JDK 默认的配置创建一个客户端
@@ -64,13 +62,11 @@ System.out.println("响应体: " + response.body());
 
 ## 二、GET 请求
 
-### 核心概念
-
-GET 是最常用的 HTTP 方法,用于从服务器获取数据,且**不携带请求体**。用 JDK HTTP Client 发送 GET 请求时,有三种常见写法:
+GET 是最常用的 HTTP 方法,用来从服务器拿数据,不带请求体。用 JDK HTTP Client 发 GET 请求,常见写法有三种:
 
-1. **不带参数的 GET**:`.uri(url)` + `.GET()`,例如查询用户列表;
-2. **带路径参数的 GET**:把 id 等参数拼进 URL 路径,例如查询单个用户 `/api/users/1`;
-3. **带查询字符串的 GET**:`?key=value` 跟在 URI 后面,服务端按查询条件过滤。
+1. 不带参数的 GET:`.uri(url)` + `.GET()`,比如查用户列表;
+2. 带路径参数的 GET:把 id 拼进 URL 路径,比如查单个用户 `/api/users/1`;
+3. 带查询字符串的 GET:`?key=value` 跟在 URI 后面,服务端按条件过滤。
 
 | 要点 | 说明 |
 |------|------|
@@ -82,7 +78,7 @@ GET 是最常用的 HTTP 方法,用于从服务器获取数据,且**不携
 
 ### 示例代码
 
-见 `GetExample.java`,核心代码如下
+代码都在 `GetExample.java` 里
 
 ```java
 // 不带参数的 GET:查询所有用户
@@ -119,13 +115,13 @@ URI uri = URI.create(BASE_URL + "/api/users?account=alice01");
 
 ### 3.1 POST 请求
 
-POST 用于向服务器**提交数据**(创建资源),与 GET 最大的区别是**携带请求体**。提交 JSON 的标准流程:
+POST 用来向服务器提交数据、创建资源,和 GET 最大的区别是带请求体。提交 JSON 的标准流程:
 
-1. **序列化**:把 Java 对象转成 JSON 字符串(教程中使用 Jackson `ObjectMapper`);
-2. **设置请求头**:`Content-Type: application/json`,告诉服务器请求体格式;
-3. **构造请求体**:`BodyPublishers.ofString(json)` 把字符串包装请求体;
-4. **声明方法**:`.POST(publisher)` 指定请求方法及请求体;
-5. **处理响应**:服务端返回统一包装 `Result<UserVO>`,用 `TypeReference` 反序列化拿到具体数据对象。
+1. 序列化:把 Java 对象转成 JSON 字符串(教程里用 Jackson 的 `ObjectMapper`);
+2. 设置请求头:`Content-Type: application/json`,告诉服务器请求体格式;
+3. 构造请求体:`BodyPublishers.ofString(json)` 把字符串包装请求体;
+4. 声明方法:`.POST(publisher)` 指定请求方法和请求体;
+5. 处理响应:服务端返回统一包装 `Result<UserVO>`,用 `TypeReference` 反序列化拿到具体数据对象。
 
 | 要点 | 说明 |
 |------|------|
@@ -139,7 +135,7 @@ POST 用于向服务器**提交数据**(创建资源),与 GET 最大的区
 
 #### 示例代码
 
-见 `PostExample.java`,核心代码如下
+例子在 `PostExample.java`
 
 ```java
 // 1. 使用 Jackson 把对象序列化为 JSON 字符串
@@ -175,7 +171,7 @@ Result<UserVO> result = objectMapper.readValue(response.body(),
 
 ### 3.2 PUT 请求
 
-PUT 用于**整体更新**已有资源,与 POST 的写法几乎一致:同样携带 JSON 请求体、同样用 `BodyPublishers.ofString`,唯一区别是把 `.POST(...)` 换成 `.PUT(...)`,并把目标地址改为带路径参数的 `/api/users/{id}`。
+PUT 用来整体更新已有资源,写法跟 POST 几乎一样:同样带 JSON 请求体,同样用 `BodyPublishers.ofString`。唯一的区别是把 `.POST(...)` 换成 `.PUT(...)`,目标地址改成带 id 的 `/api/users/{id}`。
 
 | 要点 | 说明 |
 |------|------|
@@ -212,7 +208,7 @@ HttpRequest request = HttpRequest.newBuilder()
 
 ### 3.3 DELETE 请求
 
-DELETE 用于**删除资源**,与 GET 一样**不携带请求体**,只需把 id 拼进路径。Builder 提供 `.DELETE()` 快捷方法(内部天然使用 `BodyPublishers.noBody()`)
+DELETE 用来删除资源,和 GET 一样不带请求体,把 id 拼进路径即可。Builder 提供了 `.DELETE()` 快捷方法,内部用的就是 `BodyPublishers.noBody()`
 
 | 要点 | 说明 |
 |------|------|
@@ -223,7 +219,7 @@ DELETE 用于**删除资源**,与 GET 一样**不携带请求体**,只需把
 
 #### 示例代码
 
-见 `PutDeleteExample.java`,核心代码如下
+看 `PutDeleteExample.java` 里的 DELETE 部分
 
 ```java
 HttpRequest request = HttpRequest.newBuilder()
@@ -247,11 +243,9 @@ HttpResponse<String> response =
 
 ## 四、请求头
 
-### 核心概念
+先澄清一个误区:JDK 的 `HttpClient.Builder` 没有「设置默认请求头」的方法。(可用 `javap --module java.net.http java.net.http.HttpClient$Builder` 验证,Builder 只提供 `cookieHandler / connectTimeout / executor / followRedirects / priority / proxy / authenticator / sslContext / version` 等配置。)
 
-**先澄清一个误区**:JDK 的 `HttpClient.Builder` **没有**「设置默认请求头」的方法。(可通过 `javap --module java.net.http java.net.http.HttpClient$Builder` 查看,Builder 只提供 `cookieHandler / connectTimeout / executor / followRedirects / priority / proxy / authenticator / sslContext / version` 等配置。)
-
-因此请求头只能在 **HttpRequest 构造阶段**配置,JDK 提供了三个相关 API:
+所以请求头只能在 HttpRequest 构造阶段配置。JDK 给了三个 API:
 
 | 方法 | 作用 |
 |------|------|
@@ -259,13 +253,13 @@ HttpResponse<String> response =
 | `.headers(k1, v1, k2, v2, ...)` | 一次追加多组请求头,参数个数必须为偶数 |
 | `.setHeader(key, value)` | 与 header 不同,会**覆盖**已存在的同名头 |
 
-**「Client 级默认头」的变通方案**:既然 HttpClient 不能配置默认头,业界通用做法是提供一个工厂方法,统一返回「已预置公共请求头」的 `HttpRequest.Builder`,让同一客户端发出的所有请求都携带公共头(如 `User-Agent`、`Accept`),达到集中管理的效果
+「Client 级默认头」的变通方案:既然 HttpClient 不能配置默认头,一份通用的做法是提供一个工厂方法,统一返回「已预置公共请求头」的 `HttpRequest.Builder`,同一个客户端发出的请求都能带上公共头(比如 `User-Agent`、`Accept`),集中管理
 
-**响应头读取**:`response.headers()` 返回 `HttpHeaders` 对象,常用方法:`firstValue(name)`(返回 `Optional<String>`)、`allValues(name)`(返回 `List<String>`)。
+响应头读取:`response.headers()` 返回 `HttpHeaders` 对象,常用方法:`firstValue(name)`(返回 `Optional<String>`)、`allValues(name)`(返回 `List<String>`)。
 
 ### 示例代码
 
-见 `HeaderExample.java`,核心代码如下
+见 `HeaderExample.java`:
 
 ```java
 // 单个请求头
@@ -324,9 +318,7 @@ public HttpRequest.Builder newBuilderWithDefaultHeaders() {
 
 ## 五、请求体
 
-### 核心概念
-
-请求体由 **BodyPublisher** 描述,由 `POST(BodyPublisher)`(或 PUT/PATCH)传入请求构造器中。JDK 内置了多种 BodyPublisher 实现:
+请求体由 `BodyPublisher` 描述,通过 `POST(BodyPublisher)`(PUT/PATCH 也一样)传给请求构造器。JDK 内置了多种实现:
 
 | 发布器 | 说明 | 典型场景 |
 |--------|------|----------|
@@ -336,7 +328,7 @@ public HttpRequest.Builder newBuilderWithDefaultHeaders() {
 | `BodyPublishers.ofFile(Path)` | 文件请求体 | 直接以文件为请求体 |
 | `BodyPublishers.noBody()` | 无请求体 | GET/DELETE 等无体请求 |
 
-**multipart/form-data 文件上传**:JDK HttpClient 没有内置 multipart 支持,按 RFC 2046 规范手工拼接请求体即可,格式如下
+multipart/form-data 文件上传:JDK HttpClient 没有内置 multipart 支持,按 RFC 2046 规范手工拼请求体即可,格式是这样
 
 ```text
 --boundary\r\n
@@ -347,11 +339,11 @@ Content-Type: text/plain\r\n
 --boundary--\r\n
 ```
 
-注意:`Content-Type` 请求头中携带的 boundary 必须与请求体中使用的 boundary 完全一致,服务器才能正确切分字段。
+注意:`Content-Type` 请求头里的 boundary 和请求体里用的是同一个,两边不一致的话服务器就没法正确切分字段。
 
 ### 示例代码
 
-见 `BodyExample.java`,核心代码如下
+代码见 `BodyExample.java`:
 
 ```java
 // ofString:字符串 JSON 请求体
@@ -395,20 +387,18 @@ HttpRequest request3 = HttpRequest.newBuilder()
 
 ## 六、文件下载
 
-### 核心概念
-
-下载与上传相反,服务端以**二进制流**返回文件内容。JDK HTTP Client 本身对「如何消费响应体」并不关心,区别只在于传入的**响应处理器**。常用的三种:
+下载和上传相反:服务器把文件内容按二进制流返回。HttpClient 本身不关心响应体怎么消费,区别全在传入的响应处理器上。常用的有三种:
 
 | 处理器 | 得到的内容 | 适用场景 |
 |--------|-----------|----------|
-| `BodyHandlers.ofFile(Path)` | 直接把响应体**写入本地文件** | 大文件下载,不占堆内存 |
+| `BodyHandlers.ofFile(Path)` | 直接把响应体写入本地文件 | 大文件下载,不占堆内存 |
 | `BodyHandlers.ofByteArray()` | `byte[]` | 文件较小,想整体读入内存处理 |
 | `BodyHandlers.ofInputStream()` | `InputStream` | 流式读取,边读边处理/转发 |
 
-下载流程分两步:
+下载分两步:
 
-1. **先上传拿文件名**:调用 `POST /api/files/upload` 得到 `FileVO`,其中的 `storedFileName` 是服务端磁盘上的文件名;
-2. **再按文件名下载**:GET `/api/files/download/{storedFileName}`,服务端返回文件内容二进制流
+1. 先上传拿文件名:调用 `POST /api/files/upload` 得到 `FileVO`,其中的 `storedFileName` 就是服务器磁盘上的文件名;
+2. 再按文件名下载:GET `/api/files/download/{storedFileName}`,服务器把文件内容作为二进制流返回
 
 > 响应头里的 `Content-Disposition` 携带 `attachment` 标记与原文件名,可通过 `response.headers().firstValue("Content-Disposition")` 读取;注意本服务返回的文件名是 storedFileName 去掉扩展名后的部分。
 
@@ -449,11 +439,9 @@ HttpResponse<InputStream> resp3 = httpClient.send(request,
 
 ## 七、同步与异步
 
-### 核心概念
-
-HttpClient 提供两种发送请求的方式:
+HttpClient 有同步、异步两种发送方式:
 
-**1. 同步 `send()`** — 阻塞当前线程直到收到完整响应:
+1. 同步 `send()`:阻塞当前线程直到收到完整响应。
 
 ```java
 HttpResponse<String> response =
@@ -464,7 +452,7 @@ HttpResponse<String> response =
 - 需处理 `IOException`(网络/IO 失败)与 `InterruptedException`(线程中断);
 - 适用于请求少的场景,简单直观。
 
-**2. 异步 `sendAsync()`** — 立即返回 `CompletableFuture<HttpResponse>`,请求在 HttpClient 的内部线程池中执行,不阻塞调用线程:
+2. 异步 `sendAsync()`:立即返回 `CompletableFuture<HttpResponse>`,请求在内部线程池里执行,不阻塞调用线程。
 
 ```java
 CompletableFuture<HttpResponse<String>> future =
@@ -488,7 +476,7 @@ future.thenApply(resp -> ...);                    // 回调式处理,不阻塞
 
 ### 示例代码
 
-见 `SyncAsyncExample.java`,核心代码如下
+以 `SyncAsyncExample.java` 为例
 
 ```java
 // 同步发送
@@ -534,11 +522,9 @@ return futures.stream()
 
 ## 八、响应处理器
 
-### 核心概念
-
-`BodyHandler` 决定「如何消费响应体」:拿到响应头(状态码等)后,它返回一个 `BodySubscriber`,后者把响应体字节流转换为目标类型 T。发送请求时作为第二个参数传入:`client.send(request, bodyHandler)`。
+`BodyHandler` 决定响应体怎么消费:拿到响应头(状态码等)之后,它返回一个 `BodySubscriber`,后者把响应体的字节流转成目标类型 T。发送时作为第二个参数传入:`client.send(request, bodyHandler)`。
 
-**JDK 内置响应处理器(BodyHandlers):**
+JDK 内置的响应处理器(BodyHandlers):
 
 | 处理器 | 响应体类型 | 适用场景 |
 |--------|-----------|----------|
@@ -549,10 +535,10 @@ return futures.stream()
 | `BodyHandlers.discarding()` | Void | 只关心状态码,body() 为 null |
 | `BodyHandlers.ofLines()` | Stream\<String> | 逐行处理 |
 
-**自定义 BodyHandler:** 只需实现 `apply(ResponseInfo)` 方法返回一个 `BodySubscriber`
+自定义 BodyHandler 只需实现 `apply(ResponseInfo)`,返回一个 `BodySubscriber` 就行
 
-- **按状态码分流**:2xx 正常读取,非 2xx 用 `BodySubscribers.replacing(null)` 丢弃响应体,使 body() 返回 null;
-- **响应后处理**:用 `BodySubscribers.mapping(upstream, fn)` 把上游订阅器的结果再转一次(如加前缀标记、组装业务对象)。
+- 按状态码分流:2xx 正常读取,非 2xx 用 `BodySubscribers.replacing(null)` 丢弃响应体,body() 返回 null;
+- 响应后处理:用 `BodySubscribers.mapping(upstream, fn)` 把上游订阅器的结果再转一次(如加前缀、组装业务对象)。
 
 > 提示:自定义 `BodyHandler<T>` 通常与 `BodySubscribers.ofString` / `ofByteArray` 组合使用,实现「先读字节流、再按业务逻辑解析」的能力,是接入统一响应包装结构的标准方式。
 
@@ -595,9 +581,7 @@ BodyHandler<String> handler2 = responseInfo ->
 
 ## 九、HTTP Client 配置项
 
-### 核心概念
-
-`HttpClient.newBuilder()` 返回的 Builder 可在创建客户端时配置**通用行为**。所有配置在客户端创建后**不可修改**,因此应在创建时一次设置好。
+`HttpClient.newBuilder()` 返回的 Builder 用来配置客户端的通用行为。配置在 `build()` 之后就不能改了,所以要一次设好。用到的配置项:
 
 | 配置项 | 作用 | 备注 |
 |--------|------|------|
@@ -613,7 +597,7 @@ BodyHandler<String> handler2 = responseInfo ->
 
 ### 示例代码
 
-见 `ClientConfigExample.java`,核心代码如下
+看 `ClientConfigExample.java`
 
 ```java
 CookieManager cookieManager = new CookieManager();
@@ -628,7 +612,7 @@ HttpClient client = HttpClient.newBuilder()
         .build();
 ```
 
-配置一旦生效,可通过 getter 读取(如 `client.version()`、`client.connectTimeout()`、`client.followRedirects()` 等),方便校验
+配置生效后能用 getter 读出来校验,比如 `client.version()`、`client.connectTimeout()`、`client.followRedirects()`。
 
 ### 测试验证
 
@@ -643,9 +627,7 @@ HttpClient client = HttpClient.newBuilder()
 
 ## 十、HTTP Request 配置项
 
-### 核心概念
-
-`HttpRequest.newBuilder()` 用于构造单个请求,其可配置项如下(与 HttpClient 级配置互相独立,请求级优先级更高):
+`HttpRequest.newBuilder()` 用来构造单个请求。可配项如下,注意它们和 HttpClient 级配置互相独立,请求级的优先级更高:
 
 | 配置项 | 作用 | 备注 |
 |--------|------|------|
@@ -658,7 +640,7 @@ HttpClient client = HttpClient.newBuilder()
 | `.header / .headers / .setHeader` | 请求头 | |
 | `.copy()` | 复制 Builder | 写时复制,修改副本不影响原 Builder |
 
-**`.timeout` 与 `.connectTimeout` 的区别**
+`.timeout` 与 `.connectTimeout` 的区别
 
 | | connectTimeout(Client 级) | timeout(Request 级) |
 |---|---------------------------|-----------------------|
@@ -667,7 +649,7 @@ HttpClient client = HttpClient.newBuilder()
 
 ### 示例代码
 
-见 `RequestConfigExample.java`,核心代码如下
+看 `RequestConfigExample.java`
 
 ```java
 // 请求级超时(整个请求 3 秒)
@@ -714,7 +696,7 @@ HttpRequest copied = original.copy()
 
 ## 十一、HTTP Client 核心对象及 API 速览
 
-学完以上所有示例后会发现,JDK HTTP Client 的核心对象其实只有四个,理解了它们就掌握了全部用法
+把前面的示例都过一遍,你会发现核心对象其实只有四个,搞清它们就够用了
 
 | 核心对象 | 职责 | 获取方式 |
 |----------|------|----------|
@@ -731,8 +713,8 @@ HttpRequest copied = original.copy()
 
 ### 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`)。
+- 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\>
 
@@ -749,7 +731,7 @@ HttpRequest copied = original.copy()
 
 ### 示例代码
 
-见 `CoreApiExample.java`,核心代码如下
+参考 `CoreApiExample.java`
 
 ```java
 // 1. 创建客户端
@@ -797,13 +779,13 @@ respHeaders.map();                             // Map<String, List<String>>
 
 ## 总结
 
-JDK HTTP Client 的 API 设计简洁而统一,核心流程就是「构建请求 → 发送 → 处理响应」三步。掌握 `HttpClient`、`HttpRequest`、`HttpResponse`、`HttpHeaders` 四个核心对象,配合 `BodyHandlers` 和 `BodyPublishers` 的各种实现,就能覆盖绝大多数 HTTP 通信场景
+JDK HTTP Client 的 API 就那么几件套:`HttpClient`、`HttpRequest`、`HttpResponse`、`HttpHeaders`,加上 `BodyHandlers` 和 `BodyPublishers` 的几个实现。流程上永远是「构建请求 → 发送 → 处理响应」,没有更多花样
 
-对于日常开发,建议
+日常开发里我的建议是
 
-- **简单请求**用同步 `send()`,直观易调试;
-- **批量/高并发**用异步 `sendAsync()` + `CompletableFuture`,充分利用线程池
-- **统一响应处理**通过自定义 `BodyHandler` 实现,避免每个请求都重复反序列化逻辑
-- **默认请求头**用工厂方法模式变通实现,保持代码整洁
+- 简单请求用同步 `send()`,直观,好调试;
+- 批量、高并发的场景换 `sendAsync()` + `CompletableFuture`,把线程池用起来
+- 想统一处理响应(比如都得反序列化 `Result<T>`),写个自定义 `BodyHandler`,别每次请求都手写一遍
+- 所有请求都要带公共头时,用工厂方法返回一个预置好请求头的 Builder
 
-完整示例代码见项目 `src/main/java/space/anyi/httpClient/` 目录,测试代码见 `src/test/java/space/anyi/httpClient/` 目录。所有测试均依赖本地 API 服务,请确保服务启动后再运行
+示例代码在 `src/main/java/space/anyi/httpClient/`,测试在 `src/test/java/space/anyi/httpClient/`。跑之前记得先启动 API 服务