使用Spring WebClient发送HTTP请求 - reflectoring


Spring 5有一个响应式 Web 框架:Spring WebFlux。这旨在与现有的 Spring Web MVC API 共存,但增加对非阻塞设计的支持。使用 WebFlux,您可以构建异步 Web 应用程序,使用反应式流和函数式 API 来更好地支持并发和扩展。
作为其中的一部分,Spring 5 引入了新的WebClientAPI,取代了现有的RestTemplate客户端。使用WebClient您可以使用功能流畅的 API 发出同步或异步 HTTP 请求,该 API 可以直接集成到您现有的 Spring 配置和 WebFlux 反应式框架中。
在本文中,我们将首先介绍如何立即开始向 API 发送简单的 GET 和 POST 请求WebClient,然后讨论如何WebClient进一步在大量生产应用程序中进行高级使用。
 
如何使用WebClient发出GET请求 
首先,您首先需要向项目添加一些依赖项(如果您还没有这些依赖项)。如果您使用 Spring Boot,您可以使用spring-boot-starter-webflux,或者您可以直接安装spring-webfluxreactor- netty。
Spring WebClientAPI 必须在现有异步 HTTP 客户端库之上使用。在大多数情况下,这将是 Reactor Netty,但您也可以使用 Jetty Reactive HttpClient 或 Apache HttpComponents,或者通过构建自定义连接器来集成其他人。
安装这些后,您可以在以下位置发送您的第一个 GET 请求WebClient:

WebClient client = WebClient.create();

WebClient.ResponseSpec responseSpec = client.get()
    .uri("http://example.com")
    .retrieve();

这里发生了一些事情:
  • 我们创建一个WebClient实例
  • 我们使用WebClient实例定义请求,指定请求方法 (GET) 和 URI
  • 我们完成对请求的配置,并获得一个 ResponseSpec

这是发送请求所需的一切,但重要的是要注意此时实际上还没有发送任何请求!作为一个反应式 API,在某些尝试读取或等待响应之前,不会实际发送请求。
我们怎么做?
 
WebClient如何处理 HTTP 响应 
一旦我们发出请求,我们通常想要读取响应的内容。
在上面的例子中,我们调用.retrieve()获取ResponseSpec一个请求。这是一个异步操作,它不会阻塞或等待请求本身,这意味着在下一行请求仍处于挂起状态,因此我们还不能访问任何响应详细信息。
在我们从这个异步操作中得到一个值之前,你需要了解Reactor 中的FluxMono类型。
  • Flux

Flux表示元素流。这是一个序列,它将在未来完成(成功或错误)之前异步发出任意数量的项目(0 或更多)。
在反应式编程中,这是我们的基本面。
Flux是一个流,我们可以转换(给我们一个新的转换事件流),缓冲到列表中,减少到单个值,与其他 Flux 连接并合并,或者阻塞以等待一个值。
  • Mono

Mono 是一种特定但非常常见的Flux:类型,它将在完成之前异步发出 0 或 1 个结果。在实践中,它类似于 Java 自己的CompletableFuture:它代表一个单一的未来值。
如果您想了解更多关于这些的背景知识,请查看Spring 自己的文档,其中更详细地解释了 Reactive 类型及其与传统 Java 类型的关系。
要读取响应正文,我们需要获取Mono响应内容的(即:异步未来值)。然后我们需要以某种方式解开它,触发请求并获取响应正文内容本身,一旦它可用。
有几种不同的方法来解包异步值。首先,我们将使用最简单的传统选项,通过阻塞等待数据到达:
String responseBody = responseSpec.bodyToMono(String.class).block();

这为我们提供了一个包含响应原始正文的字符串。可以在这里传递不同的类来自动将内容解析为适当的格式,或者这里使用Flux来接收响应部分的流。
请注意,我们不会自己检查此处的状态。当我们使用 .retrieve()时.,客户端会自动为我们检查状态代码,通过为任何 4xx 或 5xx 响应抛出错误来提供合理的默认值。我们稍后也会讨论自定义状态检查和错误处理。
 
WebClient如何发送复杂的 POST 请求 
我们已经看到了如何发送一个非常基本的 GET 请求,但是如果我们想要发送一些更高级的东西怎么办?
让我们看一个更复杂的例子:

MultiValueMap<String, String> bodyValues = new LinkedMultiValueMap<>();

bodyValues.add("key", "value");
bodyValues.add(
"another-key", "another-value");

String response = client.post()
    .uri(new URI(
"https://httpbin.org/post"))
    .header(
"Authorization", "Bearer MY_SECRET_TOKEN")
    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
    .accept(MediaType.APPLICATION_JSON)
    .body(BodyInserters.fromFormData(bodyValues))
    .retrieve()
    .bodyToMono(String.class)
    .block();

正如我们在此处看到的,WebClient允许我们通过使用针对常见情况 ( .contentType(type)) 或通用键和值 ( .header(key, value)) 的专用方法来配置标头。
一般来说,最好使用专用方法,因为它们更严格的类型将帮助我们提供正确的值,并且它们还包括运行时验证以捕获各种无效配置。
此示例还显示了如何添加主体。这里有几个选项:

  • 我们可以使用.BodyInserter调用.body(),它将从表单值、多部分值、数据缓冲区或其他可编码类型为我们构建正文内容。
  • 我们可以使用 Flux(包括 Mono)调用.body(),它可以异步流式传输内容以构建请求正文。
  • 我们可以调用.bodyValue(value)直接提供字符串或其他可编码值。

其中每一个都有不同的用例。大多数不熟悉反应式流的开发人员最初会发现 Flux API 没有帮助,但是随着您在反应式生态系统中投入更多,像这样的异步流数据链将开始变得更加自然。
 
如何将 SpringWebClient引入生产
以上应该足以让您提出基本请求和阅读响应,但是如果您想在此基础上构建大量应用程序,我们还需要涵盖更多主题。
  • 读取响应头

到目前为止,我们一直专注于读取响应正文,而忽略了标头。很多时候这很好,重要的标头会为我们处理,但是您会发现许多 API 在它们的响应标头中包含有价值的元数据,而不仅仅是正文。
这些数据也可以在WebClientAPI 中轻松获取,使用.toEntity()API 可以为我们提供一个ResponseEntity,包装在Mono.
这允许我们检查响应头:
ResponseEntity<String> response = client.get()
    // ...
    .retrieve()
    .toEntity(String.class)
    .block();

HttpHeaders responseHeaders = response.getHeaders();

List<String> headerValue = responseHeaders.get(
"header-name");

  • 解析响应体

在上面的例子中,我们已经将响应作为简单的字符串来处理,但是 Spring 也可以自动将它们解析为许多更高级别的类型,只需在读取响应时指定更具体的类型,如下所示:
Mono<Person> response = client.post()
    // ...
    .retrieve()
    .bodyToMono(Person.class)

能转换哪些类是取决于HttpMessageReaders可用的类。默认情况下,支持的格式包括:

  • 任何响应都转化为String,byte,ByteBuffer,DataBuffer或Resource
  • 将application/x-www-form-urlencoded响应转换为MultiValueMap<String,String>>
  • 将multipart/form-data响应转换为MultiValueMap<String, Part>
  • 使用 Jackson 反序列化 JSON 数据(如果可用)
  • 使用 Jackson 的 XML 扩展或 JAXB(如果可用)对 XML 数据进行反序列化

这也可以在您的 Spring 应用使用HttpMessageConverter的标准配置,因此可以在您的 WebMVC 或 WebFlux 服务器代码和您的WebClient实例之间共享消息转换器。如果您使用的是 Spring Boot,则可以使用预先配置的 WebClient.Builder 实例来自动设置。
  • 手动处理响应状态

默认情况下,.retrieve()将为您检查错误状态。这对于简单的情况没有问题,但您可能会发现许多 REST API 在其状态代码中编码更详细的成功信息(例如返回 201 或 202 值),或者您希望为某些错误状态添加自定义处理的 API。
可以从 读取状态ResponseEntity,就像我们对标头所做的那样,但这仅对接受的状态有用,因为在这种情况下,错误状态会在我们收到实体之前抛出错误。
为了自己处理这些状态,我们需要添加一个onStatus处理程序。此处理程序可以匹配某些状态,并返回 一个 Mono<Throwable>(以控制抛出的特定错误)或Mono.empty()停止将该状态视为错误。
ResponseEntity response = client.get()
    // ...
    .retrieve()
   
// Don't treat 401 responses as errors:
    .onStatus(
        status -> status.value() == 401,
        clientResponse -> Mono.empty()
    )
    .toEntity(String.class)
    .block();

// Manually check and handle the relevant status codes:
if (response.getStatusCodeValue() == 401) {
   
// ...
} else {
   
// ...
}

  • 发出完全异步的请求

到目前为止,我们针对每个响应调用了.block(),完全阻塞线程以等待响应到达。
在传统的重线程架构中可能很自然地适合,但在非阻塞设计中,我们需要尽可能避免这些类型的阻塞操作。
作为替代方案,我们可以通过围绕我们的Mono或Flux值编织转换来处理请求,以在返回值时处理和组合值,然后将这些Flux包装的值传递给其他非阻塞 API,所有这些都是完全异步的。
这里没有空间从头开始完全解释这种范式或 WebFlux,但这样做的示例WebClient可能如下所示:
@GetMapping("/user/{id}")
private Mono<User> getUserById(@PathVariable String id) {
    
// 异步加载一些用户数据,例如从数据库:
    Mono<BaseUserInfo> userInfo = getBaseUserInfo(id);

     
// 从单独的API使用WebClient加载用户数据:
    Mono<UserSubscription> userSubscription = client.get()
        .uri(
"http://subscription-service/api/user/" + id)
        .retrieve()
        .bodyToMono(UserSubscription.class);

     
// 组合单子:当它们都完成时,获取
    
// 并将其组合成一个用户对象。
    Mono<User> user = userInfo
        .zipWith(userSubscription)
        .map((tuple) -> new User(tuple.getT1(), tuple.getT2());

        
// 组合数据的结果可以立即返回,
    
// 无需等待或阻止,WebFlux将处理发送
    
// 稍后,在所有数据就绪后,将进行响应:
    return user;
}

 
使用 Spring 进行测试 WebTestClient
Spring 5 还包括WebTestClient,它提供了一个与 极其相似的接口WebClient,但旨在方便测试服务器端点。
我们可以通过创建一个WebTestClient绑定到服务器并通过 HTTP 发送真实请求,或者绑定到单个Controller,RouterFunction或WebHandler,使用模拟请求和响应对象运行集成测试来设置它。
看起来像这样:
/ Connect to a real server over HTTP:
WebTestClient client = WebTestClient
    .bindToServer()
    .baseUrl("http://localhost:8000")
    .build();

// Or connect to a single WebHandler using mock objects:
WebTestClient client = WebTestClient
    .bindToWebHandler(handler)
    .build();

一旦我们创建了一个 WebTestClient,我们就可以像定义任何其他WebClient.
要发送请求并检查结果,我们调用.exchange()并使用那里可用的断言方法:
client.get()
    .uri("/api/user/123")
    .exchange()
    .expectStatus().isNotFound();
// Assert that this is a 404 response

 
WebClient使用 HTTP 工具包检查和模拟HTTP 流量
部署WebClient代码后,您需要能够调试它。HTTP 请求通常是复杂交互中的关键,它们可能会以许多有趣的方式失败。能够查看您的客户端正在处理的请求和响应以了解您的系统正在做什么非常有用,并且注入您自己的数据或错误可能是一种强大的手动测试技术。
为此,您可以使用HTTP Toolkit,这是一个跨平台的开源工具,可以捕获来自各种 Java HTTP 客户端的流量,并且包含一个特定的集成来自动拦截 Spring WebClient。
一旦您安装了 HTTP Toolkit,下一步就是拦截您的 Java HTTP 流量。为此,您可以:

  • 单击 HTTP Toolkit 中的“Fresh Terminal”按钮打开一个终端,并从那里启动您的应用程序;或者
  • 正常启动您的应用程序,然后单击 HTTP Toolkit 中的“附加到 JVM”按钮以附加到已经运行的 JVM

拦截流量后,您可以从 HTTP 工具包内的“查看”页面检查应用程序发送的每个请求和响应
您还可以从“模拟”页面添加规则,以交互方式模拟 HTTP 响应、断点请求或注入连接失败和超时等错误。
 
结论
在本文中,我们研究了开始使用 Spring 所需的一切WebClient。WebFluxWebClient是成熟的强大 API,在经典的 Spring 功能集之上提供了很多功能,所以今天就在您的应用程序中尝试一下吧。