baeldung 2024-01-08
1. 概述
在本教程中,我们将探索 Java 11 中标准化的 HTTP 客户端 API,该 API 支持 HTTP/2 和 WebSocket。
其目标是取代自 Java 早期就存在于 JDK 中的旧版 HttpURLConnection 类。
在此之前,Java 仅提供了 HttpURLConnection API,这是一个底层、功能有限且不够友好的接口。
因此,开发者通常会使用一些广泛流行的第三方库,例如 Apache HttpClient、Jetty 和 Spring 的 RestTemplate。
2. 背景
这一变更作为 JEP 321 的一部分被实现。
2.1 JEP 321 引入的主要变化
- Java 9 中引入的孵化版 HTTP API 现已正式纳入 Java SE 标准 API。新的 HTTP API 位于
java.net.http.*包中。 - 新版本的 HTTP 协议旨在提升客户端发送请求和服务器返回响应的整体性能。这通过引入多项新特性实现,例如流多路复用(stream multiplexing)、头部压缩(header compression) 和 推送承诺(push promises)。
- 从 Java 11 开始,该 API 已完全支持异步操作(而之前的 HTTP/1.1 实现是阻塞式的)。异步调用通过
CompletableFuture实现。CompletableFuture负责在前一阶段完成后自动执行下一阶段,从而实现整个流程的异步化。 - 新的 HTTP 客户端 API 提供了一种标准方式来执行 HTTP 网络操作,并原生支持现代 Web 特性(如 HTTP/2),无需引入第三方依赖。
- 新 API 原生支持 HTTP/1.1、HTTP/2 和 WebSocket。核心类和接口包括:
HttpClient类(java.net.http.HttpClient)HttpRequest类(java.net.http.HttpRequest)HttpResponse<T>接口(java.net.http.HttpResponse)WebSocket接口(java.net.http.WebSocket)
2.2 Java 11 之前 HTTP 客户端的问题
现有的 HttpURLConnection API 及其实现有诸多问题:
URLConnectionAPI 最初设计时支持多种协议(如 FTP、gopher 等),但这些协议如今已不再使用。- 该 API 早于 HTTP/1.1 出现,过于抽象。
- 仅支持阻塞模式(即每个请求/响应都需要一个独立线程)。
- 难以维护。
3. HTTP Client API 概览
与 HttpURLConnection 不同,新的 HTTP Client 同时支持同步和异步请求机制。
API 由三个核心类组成:
HttpRequest:表示要通过HttpClient发送的请求。HttpClient:作为多个请求共享的配置容器。HttpResponse:表示HttpRequest调用的结果。
接下来我们将逐一深入探讨。首先,我们关注请求本身。
4. HttpRequest
HttpRequest 是一个代表我们要发送的请求的对象。可通过 HttpRequest.Builder 创建新实例。
我们可以通过调用 HttpRequest.newBuilder() 获取构建器。该构建器提供了一系列方法用于配置请求。
下面介绍其中最重要的几个。
注意:从 JDK 16 开始,新增了
HttpRequest.newBuilder(HttpRequest request, BiPredicate<String, String> filter)方法,它可以从一个已有的HttpRequest复制初始状态创建一个新的Builder。
这个构建器可用于构建一个等效于原始请求的新请求,同时允许在构造前修改请求状态,例如移除某些头部:HttpRequest.newBuilder(request, (name, value) -> !name.equalsIgnoreCase("Foo-Bar"))
4.1 设置 URI
创建请求时首先要指定 URL。有两种方式:
- 使用带
URI参数的Builder构造器 - 在
Builder实例上调用uri(URI)方法
HttpRequest.newBuilder(new URI("https://postman-echo.com/get"))
HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
完成 URI 设置后,还需指定 HTTP 方法。
4.2 指定 HTTP 方法
可通过调用构建器中的以下方法定义请求使用的 HTTP 方法:
GET()POST(BodyPublisher body)PUT(BodyPublisher body)DELETE()
稍后我们将详细介绍 BodyPublisher。
现在先看一个简单的 GET 请求示例:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.GET()
.build();
此请求已包含 HttpClient 所需的所有基本参数。
但有时我们还需要添加额外参数,例如:
- HTTP 协议版本
- 请求头(Headers)
- 超时时间(Timeout)
4.3 设置 HTTP 协议版本
API 默认充分利用 HTTP/2 协议,但我们也可以显式指定要使用的协议版本:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.version(HttpClient.Version.HTTP_2)
.GET()
.build();
重要提示:如果服务器不支持 HTTP/2,客户端会自动回退到 HTTP/1.1。
4.4 设置请求头
若需添加额外请求头,可使用构建器提供的方法:
- 通过
headers()方法一次性传入多个键值对 - 或使用
header()方法逐个添加
// 方式一:批量设置
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.headers("key1", "value1", "key2", "value2")
.GET()
.build();
// 方式二:逐个设置
HttpRequest request2 = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.header("key1", "value1")
.header("key2", "value2")
.GET()
.build();
4.5 设置超时时间
我们可以定义等待响应的最大时间。若超时,则抛出 HttpTimeoutException。默认超时时间为无限。
通过 Duration 对象调用构建器的 timeout() 方法设置:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.timeout(Duration.of(10, SECONDS))
.GET()
.build();
5. 设置请求体(Request Body)
可通过构建器方法 POST()、PUT() 和 DELETE() 添加请求体。
新 API 提供了多种内置的 BodyPublisher 实现,简化了请求体的传递:
StringProcessor:从字符串读取内容,通过HttpRequest.BodyPublishers.ofString()创建InputStreamProcessor:从InputStream读取,通过HttpRequest.BodyPublishers.ofInputStream()创建ByteArrayProcessor:从字节数组读取,通过HttpRequest.BodyPublishers.ofByteArray()创建FileProcessor:从指定路径的文件读取,通过HttpRequest.BodyPublishers.ofFile()创建
若不需要请求体,可传入 HttpRequest.BodyPublishers.noBody():
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
注意:JDK 16 新增了
HttpRequest.BodyPublishers.concat(BodyPublisher...)方法,可用于将多个发布器的内容拼接成一个请求体。
5.1 StringBodyPublisher
使用任何 BodyPublishers 实现设置请求体都非常简单直观。
例如,若要传递一个字符串作为请求体,可使用 StringBodyPublishers:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString("Sample request body"))
.build();
5.2 InputStreamBodyPublisher
由于 InputStream 需要延迟创建,因此需以 Supplier 形式传入,这与 StringBodyPublisher 略有不同,但仍很直观:
byte[] sampleData = "Sample request body".getBytes();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers
.ofInputStream(() -> new ByteArrayInputStream(sampleData)))
.build();
这里使用了 ByteArrayInputStream,当然也可以是任何 InputStream 实现。
5.3 ByteArrayProcessor
也可直接传入字节数组:
byte[] sampleData = "Sample request body".getBytes();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofByteArray(sampleData))
.build();
5.4 FileProcessor
处理文件时,可使用 FileProcessor。其工厂方法接收文件路径并从中创建请求体:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.fromFile(
Paths.get("src/test/resources/sample.txt")))
.build();
至此,我们已掌握如何创建 HttpRequest 并设置各种参数。
接下来深入探讨负责发送请求和接收响应的 HttpClient 类。
6. HttpClient
所有请求都通过 HttpClient 发送,可通过 HttpClient.newBuilder() 或 HttpClient.newHttpClient() 实例化。
它提供了许多有用且自解释的方法来处理请求/响应。
6.1 处理响应体
与创建发布器的流畅方法类似,也有专门用于创建常见响应体处理器的方法:
BodyHandlers.ofByteArrayBodyHandlers.ofStringBodyHandlers.ofFileBodyHandlers.discardingBodyHandlers.replacingBodyHandlers.ofLinesBodyHandlers.fromLineSubscriber
注意:这里使用了新的
BodyHandlers工厂类。
在 Java 11 之前,我们需要这样写:
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandler.asString());
现在可以简化为:
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
6.2 设置代理(Proxy)
可通过在构建器上调用 proxy() 方法定义连接代理:
HttpResponse<String> response = HttpClient
.newBuilder()
.proxy(ProxySelector.getDefault())
.build()
.send(request, BodyHandlers.ofString());
此例中使用了系统默认代理。
6.3 设置重定向策略
当请求的页面已移动到新地址时,我们会收到 HTTP 3xx 状态码(通常附带新 URI 信息)。
若设置了适当的重定向策略,HttpClient 可自动将请求重定向到新 URI。
通过构建器的 followRedirects() 方法设置:
HttpResponse<String> response = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build()
.send(request, BodyHandlers.ofString());
所有策略均在 HttpClient.Redirect 枚举中定义和描述。
6.4 为连接设置认证器(Authenticator)
Authenticator 是一个用于协商连接凭据(HTTP 认证)的对象,支持多种认证方案(如基本认证或摘要认证)。
大多数情况下,认证需要用户名和密码。
可使用 PasswordAuthentication 类(仅用于持有这些值):
HttpResponse<String> response = HttpClient.newBuilder()
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(
"username",
"password".toCharArray());
}
}).build()
.send(request, BodyHandlers.ofString());
注意:此例中用户名和密码以明文形式传递,生产环境中应采用更安全的方式。
并非每个请求都应使用相同的凭据。Authenticator 类提供了多种 getXXX() 方法(如 getRequestingSite()),可用于确定应提供哪些值。
6.5 发送请求:同步 vs 异步
新的 HttpClient 提供两种发送请求的方式:
send(...):同步(阻塞,直到收到响应)sendAsync(...):异步(非阻塞,不等待响应)
此前,send(...) 方法会自然等待响应:
HttpResponse<String> response = HttpClient.newBuilder()
.build()
.send(request, BodyHandlers.ofString());
此调用返回 HttpResponse 对象,且可确保只有在收到响应后才会执行下一条指令。
但在处理大量数据时,这种方式存在诸多弊端。
现在可使用 sendAsync(...) 方法(返回 CompletableFuture<HttpResponse>)进行异步处理:
CompletableFuture<HttpResponse<String>> response = HttpClient.newBuilder()
.build()
.sendAsync(request, HttpResponse.BodyHandlers.ofString());
新 API 还能处理多个响应,并支持请求和响应体的流式传输:
List<URI> targets = Arrays.asList(
new URI("https://postman-echo.com/get?foo1=bar1"),
new URI("https://postman-echo.com/get?foo2=bar2"));
HttpClient client = HttpClient.newHttpClient();
List<CompletableFuture<String>> futures = targets.stream()
.map(target -> client
.sendAsync(
HttpRequest.newBuilder(target).GET().build(),
HttpResponse.BodyHandlers.ofString())
.thenApply(response -> response.body()))
.collect(Collectors.toList());
6.6 为异步调用设置 Executor
可定义一个 Executor,为异步调用提供线程。例如,可限制处理请求所用的线程数:
ExecutorService executorService = Executors.newFixedThreadPool(2);
CompletableFuture<HttpResponse<String>> response1 = HttpClient.newBuilder()
.executor(executorService)
.build()
.sendAsync(request, HttpResponse.BodyHandlers.ofString());
CompletableFuture<HttpResponse<String>> response2 = HttpClient.newBuilder()
.executor(executorService)
.build()
.sendAsync(request, HttpResponse.BodyHandlers.ofString());
默认情况下,HttpClient 使用 Executors.newCachedThreadPool()。
6.7 定义 CookieHandler
使用新 API 和构建器,为连接设置 CookieHandler 非常简单。可通过构建器方法 cookieHandler(CookieHandler cookieHandler) 定义客户端特定的 CookieHandler。
例如,定义一个完全不允许接受 Cookie 的 CookieManager(CookieHandler 的具体实现,将 Cookie 存储与接受/拒绝策略分离):
HttpClient.newBuilder()
.cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_NONE))
.build();
若 CookieManager 允许存储 Cookie,可通过以下方式访问:
((CookieManager) httpClient.cookieHandler().get()).getCookieStore()
现在,我们聚焦于 HTTP API 中的最后一个类——HttpResponse。
7. HttpResponse 对象
HttpResponse 类表示服务器的响应,提供了许多有用方法,其中最重要的两个是:
statusCode():返回响应的状态码(int类型)(可能的值在HttpURLConnection类中定义)body():返回响应体(返回类型取决于传给send()方法的BodyHandler参数)
响应对象还有其他有用方法,如 uri()、headers()、trailers() 和 version()。
7.1 响应对象的 URI
响应对象的 uri() 方法返回接收响应的实际 URI。
有时它可能与请求对象中的 URI 不同,因为可能发生重定向:
assertThat(request.uri().toString(), equalTo("http://stackoverflow.com"));
assertThat(response.uri().toString(), equalTo("https://stackoverflow.com/"));
7.2 响应头
可通过在响应对象上调用 headers() 方法获取响应头:
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
HttpHeaders responseHeaders = response.headers();
返回 HttpHeaders 对象,表示 HTTP 头部的只读视图,并提供了一些简化查找头部值的实用方法。
7.3 响应协议版本
version() 方法定义了与服务器通信所使用的 HTTP 协议版本。
即使我们指定使用 HTTP/2,服务器仍可能通过 HTTP/1.1 响应。
实际使用的版本会在响应中指明:
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.version(HttpClient.Version.HTTP_2)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.version(), equalTo(HttpClient.Version.HTTP_1_1));
8. 处理 HTTP/2 中的推送承诺(Push Promises)
新的 HttpClient 通过 PushPromiseHandler 接口支持推送承诺。
它允许服务器在客户端请求主资源的同时,“推送”额外资源,从而减少往返次数,提升页面渲染性能。
正是 HTTP/2 的多路复用特性使我们无需再进行资源打包。对于每个资源,服务器会向客户端发送一个特殊的请求,称为“推送承诺”。
收到的推送承诺(如果有)由指定的 PushPromiseHandler 处理。若 PushPromiseHandler 为 null,则拒绝所有推送承诺。
HttpClient 提供了一个重载的 sendAsync 方法来处理此类承诺。
首先创建一个 PushPromiseHandler:
private static PushPromiseHandler<String> pushPromiseHandler() {
return (HttpRequest initiatingRequest,
HttpRequest pushPromiseRequest,
Function<HttpResponse.BodyHandler<String>,
CompletableFuture<HttpResponse<String>>> acceptor) -> {
acceptor.apply(BodyHandlers.ofString())
.thenAccept(resp -> {
System.out.println(" Pushed response: " + resp.uri() + ", headers: " + resp.headers());
});
System.out.println("Promise request: " + pushPromiseRequest.uri());
System.out.println("Promise request: " + pushPromiseRequest.headers());
};
}
然后使用 sendAsync 方法处理该推送承诺:
httpClient.sendAsync(pageRequest, BodyHandlers.ofString(), pushPromiseHandler())
.thenAccept(pageResponse -> {
System.out.println("Page response status code: " + pageResponse.statusCode());
System.out.println("Page response headers: " + pageResponse.headers());
String responseBody = pageResponse.body();
System.out.println(responseBody);
})
.join();
9. 结论
本文介绍了 Java 11 中标准化的 HTTP Client API。该 API 在 Java 9 中作为孵化功能引入,并在 Java 11 中得到进一步增强和正式标准化,提供了更强大、更现代化的 HTTP 客户端功能。