Java HttpClient 实战:用内置客户端调用 REST API

Java 11 起,JDK 自带了全新的 HttpClient,彻底取代笨重的 HttpURLConnection。它原生支持同步与异步、HTTP/2、请求/响应体流式处理,是 Java 后端调用第三方 REST API 的首选方案。本文用可运行的示例讲透它的完整用法。

一、为什么不用 HttpURLConnection 了

老旧的 HttpURLConnection 设计于 JDK 1.1,API 繁琐、默认不支持 HTTP/2、异步能力基本为零,每次请求都要写一堆样板代码。Java 11 的 HttpClient 把这些痛点一次性解决:链式 Builder 风格、内置异步模型、自动协商 HTTP/2。

维度HttpURLConnectionJava 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、超时、异步与代理这几块,你就能用零依赖的方式稳稳打通任何外部接口。

上一篇 SELinux 导致服务异常:实战排查与修复
下一篇 Kubernetes CronJob 实战:调度与失败重试
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]