HTTP Client 常用 API
学习目标
- 会构建 GET、POST 请求并读取状态码、响应头和响应体。
- 能区分连接超时、请求超时、异步异常和 HTTP 非 2xx。
- 能选择字符串、字节、文件、行流响应处理器,并正确取消异步请求。
核心知识点
专业术语
- HttpClient:JDK 11 引入的可复用 HTTP 客户端,支持 HTTP/1.1、HTTP/2 和异步调用。
- HttpRequest:不可变请求描述,包含 URI、方法、头、超时和请求体发布器。
- HttpResponse:一次响应的状态码、头、请求信息和类型化响应体。
- BodyHandler/BodyPublisher:决定响应如何落地、请求体如何产生的策略对象。
白话解释与边界
HttpClient 适合标准 HTTP 调用,但不等于业务 SDK:调用方仍需校验状态码、Content-Type、响应大小、JSON 字段和重试幂等性。客户端可以复用,Request 每次创建;连接超时和 Request timeout 应同时按业务设置。异步只是不阻塞调用线程,异常仍需在 CompletableFuture 链中处理。
常用用法
HttpClient.newBuilder:设置连接超时
连接超时覆盖建立连接阶段,DNS、TLS、服务器处理和响应读取仍可能耗时;不要把它当成完整请求超时。
// 说明:HttpClient.newBuilder:设置连接超时。
import java.net.http.HttpClient;
import java.time.Duration;
public class HttpClientTimeoutDemo {
public static void main(String[] args) {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
// 关键变化:.connectTimeout(Duration.ofSeconds(3)) .build();;调用 build,实参为 无显式参数;按已配置的超时和重定向策略构建 HttpClient。
// 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
System.out.println(client.connectTimeout().orElseThrow().toSeconds());
// 输出:3
}
}HttpClient.Redirect:配置重定向
NORMAL 遵循常见浏览器式重定向规则,ALWAYS 更激进,NEVER 交给业务处理;跨域重定向还要重新审视凭证和敏感请求头。
// 说明:HttpClient.Redirect:配置重定向。
import java.net.http.HttpClient;
public class HttpRedirectDemo {
public static void main(String[] args) {
var client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// 关键变化:.followRedirects(HttpClient.Redirect.NORMAL) .build();;调用 build,实参为 无显式参数;按已配置的超时和重定向策略构建 HttpClient。
// 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
System.out.println(client.followRedirects());
// 输出:NORMAL
}
}构建 GET 请求:设置 URI 后完成请求
Request 是不可变对象,构建后可以安全地交给同步或异步发送;URI 要在边界校验 scheme、host、端口和允许的重定向范围。
// 说明:构建 GET 请求:设置 URI 后完成请求。
import java.net.URI;
import java.net.http.HttpRequest;
public class HttpGetRequestDemo {
public static void main(String[] args) {
var request = HttpRequest.newBuilder(URI.create("https://example.com/items"))
.header("Accept", "application/json")
.GET()
.build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com/items")) .header("Accept", "application/json") .GET() .build();;按 "https://example.com/items" 调用 create 创建值,结果写入 request。
// 关键变化:.header("Accept", "application/json") .GET() .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.GET() .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
System.out.println(request.method() + " " + request.uri());
// 输出:GET https://example.com/items
}
}构建文本 POST 请求:发布字符串请求体
请求体字符串默认使用 UTF-8;真实 JSON 仍需使用可信序列化器并设置正确 Content-Type。
// 说明:构建文本 POST 请求:发布字符串请求体。
import java.net.URI;
import java.net.http.HttpRequest;
public class HttpPostRequestDemo {
public static void main(String[] args) {
var request = HttpRequest.newBuilder(URI.create("https://example.com/items"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"java\"}"))
.build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com/items")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"java\"}")) .build();;按 "https://example.com/items" 调用 create 创建值,结果写入 request。
// 关键变化:.header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"java\"}")) .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"java\"}")) .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
System.out.println(request.method());
// 输出:POST
}
}POST 等非幂等方法不能无条件自动重试。
同步发送请求:把响应体读取为字符串
send 会阻塞当前线程,适合简单同步流程;必须先检查状态码,再决定是否解析响应字符串,且要防止不受控大响应占满内存。
// 说明:同步发送请求:把响应体读取为字符串。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpSendDemo {
public static void main(String[] args) throws Exception {
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
// 输出:200
}
}检查 HTTP 响应:读取状态、响应头与正文
响应头名称不区分大小写,但一个名称可能有多个值;body 的类型由 BodyHandler 决定,状态码和业务 JSON 要分开验证。
// 说明:检查 HTTP 响应:读取状态、响应头与正文。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpResponseDemo {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + ", " + response.headers().firstValue("content-type").isPresent());
// 输出:200, true
System.out.println(response.body().isEmpty());
// 输出:true
}
}HttpRequest.timeout:请求级超时
请求超时是一次 Request 的等待边界,触发时通常以 HttpTimeoutException 表示;重试前仍要判断操作是否幂等。
// 说明:HttpRequest.timeout:请求级超时。
import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;
public class HttpRequestTimeoutDemo {
public static void main(String[] args) {
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.timeout(Duration.ofSeconds(5))
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .timeout(Duration.ofSeconds(5)) .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.timeout(Duration.ofSeconds(5)) .GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
System.out.println(request.timeout().orElseThrow().toSeconds());
// 输出:5
}
}sendAsync:异步请求与结果链
sendAsync 返回 CompletableFuture,join 会重新抛出包装后的异常;生产代码要在链上使用 exceptionally/handle,不要无条件阻塞等待所有 Future。
// 说明:sendAsync:异步请求与结果链。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpAsyncDemo {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
var future = HttpClient.newHttpClient().sendAsync(
request, HttpResponse.BodyHandlers.ofString());
// 关键变化:HttpResponse.BodyHandlers.ofString() 不接收显式参数,返回按字符串解码 HTTP 响应体的 BodyHandler。
int status = future.thenApply(HttpResponse::statusCode).join();
System.out.println(status);
// 输出:200
}
}BodyHandlers.ofByteArray:处理二进制响应
二进制响应不要强行转 String;图片、压缩数据和协议字节应使用 byte[] 或文件 BodyHandler,并设置大小保护。
// 说明:BodyHandlers.ofByteArray:处理二进制响应。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpBytesResponseDemo {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofByteArray());
System.out.println(response.body().length > 0);
// 输出:true
}
}BodyHandlers.ofFile:直接写入文件
ofFile 适合下载大响应,目标文件的覆盖、权限、磁盘空间和失败清理仍由调用方负责。
// 说明:BodyHandlers.ofFile:直接写入文件。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class HttpFileResponseDemo {
public static void main(String[] args) throws Exception {
Path target = Files.createTempFile("http-body-", ".bin");
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofFile(target));
System.out.println(response.statusCode() + ", " + (Files.size(target) > 0));
// 输出:200, true
Files.deleteIfExists(target);
// 关键变化:Files.deleteIfExists(target);;删除路径 target;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}BodyHandlers.ofLines:按行处理响应
响应行 Stream 也要关闭;它适合边读边处理,但不应在没有协议限制时无限累积到集合。
// 说明:BodyHandlers.ofLines:按行处理响应。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpLinesResponseDemo {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofLines());
try (var lines = response.body()) {
// 输入:try (var lines = response.body()) {;资源变量 lines 接收 response.body(),try 结束时自动关闭。
// 关键变化:try (var lines = response.body()) {;创建资源 lines,构造表达式为 response.body();try 结束时关闭该资源。
System.out.println(lines.findFirst().isPresent());
// 输出:true
}
}
}处理 HTTP 标头:写入请求并读取响应
认证、Cookie、Trace ID 等头必须有明确的信任边界;不要把密码、Token 或内部地址写进日志。
// 说明:处理 HTTP 标头:写入请求并读取响应。
import java.net.URI;
import java.net.http.HttpRequest;
public class HttpHeadersDemo {
public static void main(String[] args) {
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.header("Accept", "application/json")
.header("X-Request-Id", "demo-1")
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .header("Accept", "application/json") .header("X-Request-Id", "demo-1") .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.header("Accept", "application/json") .header("X-Request-Id", "demo-1") .GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.header("X-Request-Id", "demo-1") .GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
System.out.println(request.headers().firstValue("accept").orElse("missing"));
// 输出:application/json
}
}不常用但需要知道
HttpClient.Version:偏好 HTTP/2 或 HTTP/1.1
这是偏好而不是对端强制结果;HTTP/2 需要服务端、TLS 和代理链路共同支持。
import java.net.http.HttpClient;
public class HttpVersionDemo {
public static void main(String[] args) {
var client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
System.out.println(client.version());
// 输出:HTTP_1_1
}
}CompletableFuture.cancel:取消异步请求
取消是协作式的,可能已经建立连接或收到部分响应;业务代码还要停止后续解析、重试和界面更新。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
public class HttpCancelDemo {
public static void main(String[] args) {
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
var future = HttpClient.newHttpClient().sendAsync(request,
java.net.http.HttpResponse.BodyHandlers.ofString());
// 关键变化:java.net.http.HttpResponse.BodyHandlers.ofString() 不接收显式参数,返回按字符串解码 HTTP 响应体的 BodyHandler。
System.out.println(future.cancel(true));
// 输出:true
}
}BodyPublishers.ofFile:从文件上传请求体
上传要设置大小上限、内容类型和重试策略;文件变更、权限和删除时机都属于调用方责任。
import java.net.URI;
import java.net.http.HttpRequest;
import java.nio.file.Files;
public class HttpFileUploadDemo {
public static void main(String[] args) throws Exception {
var file = Files.createTempFile("http-upload-", ".txt");
Files.writeString(file, "payload");
// 关键变化:Files.writeString(file, "payload");;将 "payload" 写入路径 file;writeString 返回目标 Path,文件内容更新。
var request = HttpRequest.newBuilder(URI.create("https://example.com/upload"))
.header("Content-Type", "text/plain")
.POST(HttpRequest.BodyPublishers.ofFile(file))
.build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com/upload")) .header("Content-Type", "text/plain") .POST(HttpRequest.BodyPublishers.ofFile(file)) .build();;按 "https://example.com/upload" 调用 create 创建值,结果写入 request。
// 关键变化:.header("Content-Type", "text/plain") .POST(HttpRequest.BodyPublishers.ofFile(file)) .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.POST(HttpRequest.BodyPublishers.ofFile(file)) .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
System.out.println(request.method() + ", " + Files.size(file));
// 输出:POST, 7
Files.deleteIfExists(file);
// 关键变化:Files.deleteIfExists(file);;删除路径 file;路径存在时返回 true,不存在时返回 false;文件系统状态更新。
}
}Authenticator:代理或服务端认证回调
不要在源码中硬编码凭证;认证回调可能被多次触发,需结合 host、port、protocol 和凭证存储做限制。
import java.net.Authenticator;
import java.net.PasswordAuthentication;
public class HttpAuthenticatorDemo {
public static void main(String[] args) {
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("user", "secret".toCharArray());
// 关键变化:return new PasswordAuthentication("user", "secret".toCharArray());;调用 PasswordAuthentication,实参为 无显式参数;创建用户名为 user、密码为 secret 的 PasswordAuthentication。
}
};
System.out.println(authenticator != null);
// 输出:true
}
}自定义 BodyHandler:限制响应大小
示例展示响应大小保护的入口;生产代码还要考虑流式限制,避免先把超大响应完整收进内存再判断。
import java.net.http.HttpResponse;
public class HttpBodyHandlerBoundaryDemo {
public static void main(String[] args) {
HttpResponse.BodyHandler<byte[]> handler = info ->
HttpResponse.BodySubscribers.mapping(
HttpResponse.BodySubscribers.ofByteArray(), bytes -> {
// 关键变化:mapping(ofByteArray(), bytes -> ...) 把完整字节数组映射为目标类型的 BodySubscriber,handler 接收该处理器。
// 关键变化:HttpResponse.BodySubscribers.ofByteArray(), bytes -> {;调用 ofByteArray,实参为 无显式参数;返回收集响应字节的 BodySubscriber。
if (bytes.length > 1024) throw new IllegalStateException("too large");
return bytes;
});
System.out.println(handler != null);
// 输出:true
}
}简单案例
URLEncoder.encode:编码查询参数值
它执行 application/x-www-form-urlencoded 编码,只编码参数值,不能直接编码整条 URL。
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
String keyword = URLEncoder.encode("Java 入门", StandardCharsets.UTF_8);
String url = "https://example.test/search?q=" + keyword;
// 关键变化:String url = "https://example.test/search?q=" + keyword;;拼接完整 URL 文本 "https://example.test/search?q=" + keyword,结果写入 url。
System.out.println(keyword);
// 输出:Java+%E5%85%A5%E9%97%A8
System.out.println(url);
// 输出:https://example.test/search?q=Java+%E5%85%A5%E9%97%A8URL(String):解析绝对资源地址
URL 构造器把协议、主机、端口和路径解析为结构化地址,但不会在构造时连接服务器。固定地址可以直接构造;新代码通常优先用 URI 表达和校验地址,再在需要旧 API 时转为 URL。
import java.net.URL;
URL endpoint = new URL("https://example.test:8443/api/users");
// endpoint 只保存地址组件,这一行没有发生 DNS 查询或网络 I/O。
System.out.println(endpoint.getHost() + ":" + endpoint.getPort());
// 输出:example.test:8443URL.openConnection:创建底层 URLConnection
它只创建连接对象;真实网络访问还需读写,并应显式设置连接与读取超时。
import java.net.URL;
URL endpoint = new URL("https://example.test/api");
var connection = endpoint.openConnection();
// connection 还未读写网络;先在它上设置连接与读取超时。
connection.setConnectTimeout(3_000);
// 关键变化:connection.setConnectTimeout(3_000);;调用 setConnectTimeout,实参为 3_000;把 connection 的连接超时设置为 3_000 毫秒;连接配置更新。
connection.setReadTimeout(5_000);
// 关键变化:connection.setReadTimeout(5_000);;调用 setReadTimeout,实参为 5_000;把 connection 的读取超时设置为 5_000 毫秒;连接配置更新。
System.out.println(connection.getConnectTimeout());
// 输出:3000
System.out.println(connection.getReadTimeout());
// 输出:5000import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpClientDemo {
public static void main(String[] args) throws Exception {
var client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// 关键变化:.connectTimeout(Duration.ofSeconds(3)) .followRedirects(HttpClient.Redirect.NORMAL) .build();;调用 build,实参为 无显式参数;按已配置的超时和重定向策略构建 HttpClient。
// 关键变化:.followRedirects(HttpClient.Redirect.NORMAL) .build();;调用 build,实参为 无显式参数;按已配置的超时和重定向策略构建 HttpClient。
// 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
var request = HttpRequest.newBuilder(URI.create("https://example.com"))
.timeout(Duration.ofSeconds(5))
.header("Accept", "text/html")
.GET().build();
// 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .timeout(Duration.ofSeconds(5)) .header("Accept", "text/html") .GET().build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
// 关键变化:.timeout(Duration.ofSeconds(5)) .header("Accept", "text/html") .GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.header("Accept", "text/html") .GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
// 关键变化:.GET().build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 == 2) {
System.out.println(response.body().contains("Example Domain"));
// 输出:true
} else {
System.out.println("HTTP " + response.statusCode());
// 输出:HTTP 4xx 或 HTTP 5xx
}
}
}该案例需要网络可达;真实客户端还需按 API 契约处理错误 JSON、Content-Type、响应大小、日志脱敏和幂等重试。
易混点
connectTimeout只管连接,Request timeout 才是一次调用的等待边界;两者都不能替代业务超时。- HTTP 非 2xx 不会自动抛成业务异常,必须显式检查 statusCode。
ofString、ofByteArray、ofFile和ofLines决定内存、编码和资源行为,不能随意互换。- 异步 Future 的异常通常在
join/get或完成阶段暴露,取消也不等于对端一定停止工作。
课后小问
为什么收到 500 时
HttpClient.send通常不会直接抛 IOException? 答案:500 是 HTTP 协议层的合法响应,网络传输本身可能成功。 解析:调用方要单独检查 statusCode,并把协议错误映射为业务异常或重试决策。下载大文件为什么优先
BodyHandlers.ofFile而不是ofByteArray? 答案:ofFile 可以边接收边写文件,避免把完整响应一次性放进堆内存。 解析:仍要处理临时文件、磁盘空间、部分下载和校验,不能把文件落盘视为自动可靠。
本节小结
- HttpClient 复用连接,HttpRequest 描述调用,HttpResponse 描述结果。
- 连接超时、请求超时、状态码和业务错误要分别处理。
- BodyHandler 决定响应的内存和资源策略,BodyPublisher 决定请求体来源。
- 异步、重定向、认证和重试都必须服从安全、大小和幂等边界。
快速回顾
- GET/POST 由 Request builder 表达,BodyPublisher 提供请求体。
send阻塞,sendAsync返回 CompletableFuture。- 先看 statusCode 和 headers,再解析 body。
- 大响应优先文件或流式处理,敏感头和认证信息不能泄露。
阿源的知识库