Matthew Gilliard 2020-07-21
发起 HTTP 请求是现代编程的核心功能之一,通常也是学习一门新语言时最先尝试的事情。对于 Java 开发者而言,有多种方式可以实现——既可以使用 JDK 自带的核心库,也可以借助第三方库。本文将介绍我常用的 Java HTTP 客户端。如果你使用的是其他客户端,那也很好!欢迎告诉我。
在本文中,我会涵盖以下内容:
- 核心 Java 库:
HttpURLConnectionHttpClient(Java 11+)
- 流行第三方库:
- Apache HttpClient
- OkHttp
- Retrofit
所有示例代码均基于 Java 11,并调用 NASA 提供的「每日天文图」(Astronomy Picture of the Day, APOD)API。完整代码已托管在 GitHub 上。
使用 Java 核心 API 发起 HTTP 请求
自 Java 1.1 起,JDK 就内置了 HTTP 客户端。而在 Java 11 中,又新增了一个更现代化的客户端。如果你希望避免引入额外依赖,这两个选项可能很适合你。
Java 1.1 的 HttpURLConnection
首先,我们得面对一个哲学问题:类名中的缩写到底要不要大写?先别纠结了。闭上眼睛,回到 1997 年——《泰坦尼克号》正在横扫票房并催生无数表情包,辣妹组合(Spice Girls)发行了畅销专辑,但当年最轰动的大事,无疑是 HttpURLConnection 被加入 Java 1.1。
下面是如何使用它向 APOD API 发起 GET 请求的示例:
// 创建一个 URL 对象
URL url = new URL("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY");
// 打开连接并强制转换为 HttpURLConnection
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求方法、请求头等(此时连接已“打开”)
connection.setRequestProperty("accept", "application/json");
// 这一行真正发起请求
InputStream responseStream = connection.getInputStream();
// 使用 Jackson 手动将响应体 InputStream 转换为 APOD 对象
ObjectMapper mapper = new ObjectMapper();
APOD apod = mapper.readValue(responseStream, APOD.class);
// 最终获得响应数据
System.out.println(apod.title);
这段代码显得相当冗长,而且操作顺序令人困惑(为什么要在打开连接之后才设置请求头?)。虽然它支持 POST 请求体、自定义超时等复杂场景,但我始终觉得这个 API 不够直观。
那么,什么时候该用 HttpURLConnection 呢?如果你需要支持使用旧版 Java 的客户端,且无法添加外部依赖,那它可能是唯一选择。不过我相信这类开发者已经很少了,更多时候你可能会在老旧代码库中见到它。如需更现代的方案,请继续阅读。
Java 11 的 HttpClient
时隔二十多年,在《黑豹》热映的 2018 年,Java 11 终于引入了全新的 java.net.http.HttpClient。它拥有更合理的 API 设计,支持 HTTP/2 和 WebSocket,并且可以选择同步或异步方式发起请求(通过 CompletableFuture)。
通常,发起 HTTP 请求后我们最关心的是如何将响应体解析为对象。如果某个库在这方面做得不好,那它很难让我感到愉悦。HttpClient 通过 BodyHandler 将 HTTP 响应转换为你指定的类型。JDK 内置了一些处理器,比如 String、byte[](用于二进制数据)、按行分割的 Stream<String> 等。但没有内置的 JSON 解析器,因此我基于 Jackson 编写了一个自定义的 JsonBodyHandler(参考实现),它返回一个 Supplier<APOD>,使用时调用 .get() 即可。
同步请求示例:
// 创建客户端
var client = HttpClient.newHttpClient();
// 构建请求
var request = HttpRequest.newBuilder(
URI.create("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"))
.header("accept", "application/json")
.build();
// 发送请求并处理响应
var response = client.send(request, new JsonBodyHandler<>(APOD.class));
// 输出结果
System.out.println(response.body().get().title);
异步请求示例:
// 异步发送请求
var responseFuture = client.sendAsync(request, new JsonBodyHandler<>(APOD.class));
// 在此期间可以执行其他任务
// 阻塞等待结果(实际使用中建议用 thenApply 等非阻塞方式)
var response = responseFuture.get();
System.out.println(response.body().get().title);
第三方 Java HTTP 客户端库
如果内置客户端不能满足需求,别担心!还有许多优秀的第三方库可供选择。
Apache HttpClient
Apache 软件基金会的 HTTP 客户端历史悠久、广泛使用,也是许多高层库的基础。需要注意的是,其历史有些混乱:旧版的 Commons HttpClient 已停止维护,新版(仍叫 HttpClient)属于 HttpComponents 项目。2020 年初发布的 5.0 版本增加了对 HTTP/2 的支持,并同时支持同步与异步请求。
总体而言,它的 API 偏底层——很多功能需要你自己实现。下面的代码调用了 NASA API,看起来不难,但我省略了生产环境中必需的错误处理逻辑,并且仍需手动用 Jackson 解析 JSON。此外,若不配置日志框架,控制台可能会输出烦人的警告(虽无大碍,但看着不舒服)。
ObjectMapper mapper = new ObjectMapper();
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpGet request = new HttpGet("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY");
APOD response = client.execute(request, httpResponse ->
mapper.readValue(httpResponse.getEntity().getContent(), APOD.class));
System.out.println(response.title);
}
OkHttp
OkHttp 是 Square 公司开发的 HTTP 客户端,内置了许多实用功能,例如自动处理 GZIP 压缩、响应缓存、网络错误时自动重试或切换备用主机,以及支持 HTTP/2 和 WebSocket。其 API 简洁明了,但同样没有内置 JSON 解析,因此仍需配合 Jackson:
ObjectMapper mapper = new ObjectMapper();
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")
.build(); // 默认为 GET 请求
Response response = client.newCall(request).execute();
APOD apod = mapper.readValue(response.body().byteStream(), APOD.class);
System.out.println(apod.title);
不过,OkHttp 的真正威力在于与 Retrofit 结合使用。
Retrofit
Retrofit 同样来自 Square,构建于 OkHttp 之上。它在保留 OkHttp 所有底层能力的同时,提供了一种通过注解定义接口的方式,将 HTTP 细节抽象为简洁的 Java API。
首先,定义一个接口来描述 APOD API 的调用方法:
public interface APODClient {
@GET("/planetary/apod")
@Headers("accept: application/json")
CompletableFuture<APOD> getApod(@Query("api_key") String apiKey);
}
返回类型 CompletableFuture<APOD> 表示这是一个异步客户端(Square 也提供了其他适配器,或可自定义)。这种接口形式非常便于单元测试中的 Mock 操作。
接着,让 Retrofit 根据该接口生成实现类,并指定基础 URL:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.nasa.gov")
.addConverterFactory(JacksonConverterFactory.create())
.build();
APODClient apodClient = retrofit.create(APODClient.class);
CompletableFuture<APOD> response = apodClient.getApod("DEMO_KEY");
// 可在此执行其他任务
APOD apod = response.get();
System.out.println(apod.title);
API 认证处理
如果多个接口方法都需要传递 api_key,可以通过为 OkHttpClient 添加拦截器(Interceptor)统一处理:
private OkHttpClient clientWithApiKey(String apiKey) {
return new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request originalRequest = chain.request();
HttpUrl newUrl = originalRequest.url().newBuilder()
.addQueryParameter("api_key", apiKey).build();
Request request = originalRequest.newBuilder().url(newUrl).build();
return chain.proceed(request);
}).build();
}
我喜欢这种面向接口的 API 设计(除最简单场景外)。用类来表示远程 API 是一种优雅的抽象,能很好地融入依赖注入体系。而 Retrofit 能基于可定制的 OkHttp 客户端自动生成实现,非常强大。
其他 Java HTTP 客户端
文章发布到 Twitter 后,我欣喜地看到大家热烈讨论各自使用的 HTTP 客户端。如果你对上述选项都不满意,不妨看看这些建议:
- REST Assured:专为测试 REST 服务设计的客户端,提供流畅的链式语法和便捷的响应断言方法。
- cvurl:对 Java 11
HttpClient的封装,简化了复杂请求的编写。 - Feign:与 Retrofit 类似,可通过注解接口生成客户端,高度灵活,支持多种请求/响应处理方式、指标监控、重试机制等。
- Spring 的 RestTemplate(同步)与 WebClient(异步):如果你的项目已全面采用 Spring 生态,沿用它们是个不错的选择。
- MicroProfile Rest Client:同样基于“注解接口生成客户端”的理念,独特之处在于可复用同一接口同时构建服务端和客户端,确保两者完全匹配。如果你既要开发服务又要提供客户端,值得考虑。
总结
Java 中的 HTTP 客户端选择非常丰富:
- 简单场景:推荐使用内置的
java.net.http.HttpClient(Java 11+)。 - 复杂应用或需要 API 抽象:建议使用 Retrofit 或 Feign。