探索 Java HTTP Client

更新于 2025-12-27

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 及其实现有诸多问题:

  • URLConnection API 最初设计时支持多种协议(如 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.ofByteArray
  • BodyHandlers.ofString
  • BodyHandlers.ofFile
  • BodyHandlers.discarding
  • BodyHandlers.replacing
  • BodyHandlers.ofLines
  • BodyHandlers.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 的 CookieManagerCookieHandler 的具体实现,将 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 处理。若 PushPromiseHandlernull,则拒绝所有推送承诺。

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 客户端功能。