Java 11 起,JDK 自带了全新的 HttpClient,彻底取代笨重的 HttpURLConnection。它原生支持同步与异步、HTTP/2、请求/响应体流式处理,是 Java 后端调用第三方 REST API 的首选方案。本文用可运行的示例讲透它的完整用法。
一、为什么不用 HttpURLConnection 了
老旧的 HttpURLConnection 设计于 JDK 1.1,API 繁琐、默认不支持 HTTP/2、异步能力基本为零,每次请求都要写一堆样板代码。Java 11 的 HttpClient 把这些痛点一次性解决:链式 Builder 风格、内置异步模型、自动协商 HTTP/2。
| 维度 | HttpURLConnection | Java HttpClient(11+) |
|---|---|---|
| 代码风格 | 过程式、样板多 | 链式 Builder,清晰直观 |
| HTTP 协议 | 仅 HTTP/1.1 | 默认 HTTP/2,可降级 1.1 |
| 异步支持 | 无(需自己开线程) | 原生 sendAsync + Future |
| 超时控制 | 仅连接超时 | 连接/请求/响应细粒度超时 |
| 请求体发布 | 手写输出流 | BodyPublishers 内置多种 |
如果你的项目还停留在 JDK 8,可以引入 Apache HttpClient 或 OkHttp;但只要用 Java 11 及以上,优先考虑内置的 HttpClient,零额外依赖。
二、发起第一个 GET 请求
最简单的形式是构建一个 HttpClient,再构造一个 HttpRequest,最后用 send 同步发送并取得 HttpResponse。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class GetDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/repos/openjdk/jdk"))
.header("Accept", "application/vnd.github+json")
.GET()
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("状态码: " + response.statusCode());
System.out.println("响应体: " + response.body());
}
}
BodyHandlers.ofString() 表示把响应体读成字符串。如果响应很大,也可以换成 ofInputStream() 或 ofFile(Path) 流式处理,避免一次性占用过多内存。
三、POST 发送 JSON 请求体
调用 REST API 最常见的写操作是 POST JSON。关键在于用 BodyPublishers.ofString 发布请求体,并显式声明 Content-Type。
import java.nio.charset.StandardCharsets;
String payload = "{\"name\":\"workbuddy\",\"active\":true}";
HttpRequest postReq = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/post"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
payload, StandardCharsets.UTF_8))
.build();
HttpResponse<String> r =
client.send(postReq, HttpResponse.BodyHandlers.ofString());
System.out.println(r.statusCode());
注意务必指定 StandardCharsets.UTF_8,否则中文在非默认 UTF-8 环境下会被按平台编码发送,导致服务端解析乱码。这是线上最常见的隐式 Bug 之一。
四、处理响应:状态码、Header 与响应体
HttpResponse 提供了完整的响应信息。推荐根据状态码分支处理,并对 4xx/5xx 做兜底。
HttpResponse<String> resp =
client.send(request, HttpResponse.BodyHandlers.ofString());
switch (resp.statusCode()) {
case 200, 201 -> System.out.println("成功: " + resp.body());
case 401 -> System.out.println("未授权,检查 Token");
case 429 -> System.out.println("限流,请退避重试");
default -> System.out.println("异常: " + resp.statusCode());
}
// 读取指定响应头
String contentType = resp.headers()
.firstValue("content-type")
.orElse("unknown");
System.out.println("Content-Type: " + contentType);
resp.headers() 返回 HttpHeaders,支持 firstValue、allValues 等方法,处理分页、限流计数等场景很方便。
五、异步调用与超时控制
高并发场景下,同步 send 会占用线程。改用 sendAsync 返回 CompletableFuture,配合线程池实现非阻塞调用。如果你对 Java 异步编排还不熟悉,可以先复习 CompletableFuture 异步编排实战。
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/v1/models"))
.timeout(java.time.Duration.ofSeconds(10))
.GET()
.build();
client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(body -> System.out.println("异步结果: " + body))
.join(); // 演示用,生产环境不要随意 join 阻塞
.timeout(Duration) 既能加在请求上,也能在 Builder 阶段全局设置;超时抛出 HttpTimeoutException,应当捕获后做重试或降级。建议始终把超时显式写出,杜绝无限期挂起。
六、调用大模型 / REST 网关的实战示例
在当前 AI 化浪潮里,Java 服务经常需要调用大模型接口。下面演示如何向一个 OpenAI 兼容的网关发起聊天补全请求——这类网关我们在 One API 自建大模型 API 网关 与 FastAPI 流式大模型对话 API 中都详细讲过,Java 侧只要把 base_url 指向网关即可。
String apiKey = System.getenv("LLM_API_KEY");
String body = """
{
"model": "gpt-4o-mini",
"messages": [{"role":"user","content":"用一句话解释 HTTP/2"}]
}
""";
HttpRequest llmReq = HttpRequest.newBuilder()
.uri(URI.create("https://gateway.example.com/v1/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.timeout(java.time.Duration.ofSeconds(30))
.build();
HttpResponse<String> llmResp =
client.send(llmReq, HttpResponse.BodyHandlers.ofString());
System.out.println(llmResp.body());
用 Java 15+ 的文本块(Text Blocks)写 JSON 体可读性大幅提升。密钥务必从环境变量或配置中心读取,Dify + Ollama 本地工作流 一文也强调过不要把凭据硬编码进代码。
七、HTTPS、代理与连接复用
默认 HttpClient 已信任系统 CA 证书,访问 HTTPS 站点开箱即用。若需走公司代理或指定 HTTP 版本,可在 Builder 中配置:
import java.net.InetSocketAddress;
import java.net.ProxySelector;
HttpClient proxyClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.proxy(ProxySelector.of(
new InetSocketAddress("127.0.0.1", 7890)))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
通过 .version(HTTP_2) 可强制协商 HTTP/2,连接默认复用,无需像旧方案那样手动管理连接池。若你的网关前面挂了 Nginx 反向代理,记得在网关侧放开足够的超时与请求体大小,避免被代理层提前截断。容器化部署时参考 Docker 实战 把相关端口映射正确。
八、文件上传与下载:流式体处理
除了 JSON,HttpClient 同样擅长处理二进制体。BodyPublishers.ofFile 可以直接把本地文件作为请求体发出,适合对接对象存储、图片服务等上传接口;下载大文件时则用 BodyHandlers.ofFile 直接落盘,避免把整个响应读进内存。
import java.net.http.HttpRequest.BodyPublishers;
import java.nio.file.Path;
// 上传文件:把本地图片作为 multipart 之外的纯二进制体发送
HttpRequest upload = HttpRequest.newBuilder()
.uri(URI.create("https://files.example.com/upload"))
.header("Content-Type", "application/octet-stream")
.POST(BodyPublishers.ofFile(Path.of("/tmp/cover.png")))
.build();
// 下载文件:直接流式写入磁盘
HttpResponse<Path> dl = client.send(
HttpRequest.newBuilder().uri(URI.create("https://files.example.com/a.zip")).GET().build(),
HttpResponse.BodyHandlers.ofFile(Path.of("/tmp/a.zip")));
System.out.println("已保存到: " + dl.body());
对于真正的 multipart/form-data 上传(例如同时带表单字段和文件),JDK 没有内置便捷方法,需要自己按 RFC 7578 拼接边界,或者用 OkHttp 的 MultipartBody。但仅上传单个二进制流时,上面的 ofFile 写法最简单可靠。
九、限流重试:指数退避实战
调用第三方 API 难免遇到 429 限流或瞬时 5xx。与其盲目重试,不如封装一个带指数退避的辅助方法:首次失败等 1 秒,之后 2 秒、4 秒递增,并设最大次数与总超时。
import java.time.Duration;
static HttpResponse<String> sendWithRetry(HttpClient c, HttpRequest req, int max) throws Exception {
int attempt = 0;
while (true) {
HttpResponse<String> r = c.send(req, HttpResponse.BodyHandlers.ofString());
if (r.statusCode() == 429 || r.statusCode() >= 500) {
if (++attempt > max) return r; // 超过上限,返回最后一次结果
long wait = (long) Math.pow(2, attempt) * 1000;
System.out.println("第" + attempt + "次重试,等待 " + wait + "ms");
Thread.sleep(wait);
continue;
}
return r;
}
}
在高并发场景里,建议把退避逻辑升级为令牌桶或信号量限流,避免大量线程同时退避又同时重试造成“重试风暴”。与上一节的 sendAsync 结合,还能做到不阻塞业务线程的异步重试。
十、在 Spring Boot 中复用 HttpClient
实际项目几乎不会每次请求都 newHttpClient(),而是把它声明为单例 Bean 注入使用,统一配置超时、代理与连接池参数。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HttpConfig {
@Bean
public HttpClient httpClient() {
return HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
}
}
把 HttpClient 交给 Spring 容器管理后,所有需要对外调用的 Service 直接注入即可,既复用连接又便于统一加拦截器与监控。如果你正在搭建一个对接大模型网关的后端,这个单例 Bean 正是承载 One API 网关 客户端的最佳落点。
十一、常见坑与排障清单
| 现象 | 根因 | 解决 |
|---|---|---|
| 中文响应乱码 | 未指定 UTF-8 请求体编码 | ofString(json, UTF_8) |
| 请求永久挂起 | 没设超时 | Builder 或请求加 .timeout |
| 401 未授权 | Token 缺失/过期 | 检查 Authorization 头与环境变量 |
| 429 限流 | 调用频率过高 | 指数退避 + 限流令牌桶 |
| 代理下超时 | ProxySelector 未配置 | Builder 显式设置 proxy |
最后一条经验:生产环境务必把 sendAsync 的 CompletableFuture 接入统一的异常处理器,防止某个失败请求静默吞掉异常。配合 GitHub Actions 做接口冒烟测试,可以在每次发布前自动验证对外依赖是否可用。
结语
Java 11 的 HttpClient 用一套简洁的 Builder API 统一了同步/异步、HTTP/2 与流式体处理,是 Java 后端对接 REST API 的现代标准答案。掌握 GET/POST、超时、异步与代理这几块,你就能用零依赖的方式稳稳打通任何外部接口。




