1. 概述

WebClient 是一个简化 HTTP 请求执行的接口。与 RestTemplate 不同,它是响应式且非阻塞的客户端,能够消费和处理 HTTP 响应。尽管设计为非阻塞,它也能用于阻塞场景。

本教程将深入探讨 WebClient 接口的核心方法:retrieve()、exchangeToMono() 和 exchangeToFlux()。我们将分析这些方法的异同点,通过实际案例展示不同使用场景,并使用 JSONPlaceholder API 获取用户数据。

2. 示例环境搭建

首先,创建 Spring Boot 应用并在 pom.xml 中添加 spring-boot-starter-webflux 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>3.2.4</version>
</dependency>

该依赖提供了 WebClient 接口,使我们能够执行 HTTP 请求

接下来,查看请求 https://jsonplaceholder.typicode.com/users/1 的示例 GET 响应:

{
  "id": 1,
  "name": "Leanne Graham",
// ...
}

创建名为 User 的 POJO 类:

class User {

    private int id;
    private String name;

   // 标准构造方法、getter 和 setter

}

JSONPlaceholder API 的 JSON 响应将被反序列化并映射到 User 类实例。

最后,创建带有基础 URL 的 WebClient 实例:

WebClient client = WebClient.create("https://jsonplaceholder.typicode.com/users");

这里定义了 HTTP 请求的基础 URL。

3. exchange() 方法详解

exchange() 方法直接返回 ClientResponse,提供对 HTTP 状态码、响应头和响应体的访问。简单来说,ClientResponse 代表 WebClient 返回的 HTTP 响应。

然而,此方法自 Spring 5.3 起已被弃用,根据发射类型被 exchangeToMono() 或 exchangeToFlux() 替代。这两个方法允许我们根据响应状态解码响应。

3.1. 发射 Mono

使用 exchangeToMono() 发射 Mono 的示例:

@GetMapping("/user/exchange-mono/{id}")
Mono<User> retrieveUsersWithExchangeAndError(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .exchangeToMono(res -> {
          if (res.statusCode().is2xxSuccessful()) {
              return res.bodyToMono(User.class);
          } else if (res.statusCode().is4xxClientError()) {
              return Mono.error(new RuntimeException("Client Error: can't fetch user"));
          } else if (res.statusCode().is5xxServerError()) {
              return Mono.error(new RuntimeException("Server Error: can't fetch user"));
          } else {
              return res.createError();
           }
     });
}

上述代码根据 HTTP 状态码解码响应,获取用户信息。

3.2. 发射 Flux

使用 exchangeToFlux() 获取用户集合:

@GetMapping("/user-exchange-flux")
Flux<User> retrieveUsersWithExchange() {
   return client.get()
     .exchangeToFlux(res -> {
         if (res.statusCode().is2xxSuccessful()) {
             return res.bodyToFlux(User.class);
         } else {
             return Flux.error(new RuntimeException("Error while fetching users"));
         }
    });
}

这里使用 exchangeToFlux() 将响应体映射为 User 对象的 Flux,请求失败时返回自定义错误信息。

3.3. 直接获取响应体

exchangeToMono() 或 exchangeToFlux() 可不指定状态码直接使用:

@GetMapping("/user-exchange")
Flux<User> retrieveAllUserWithExchange(@PathVariable int id) {
    return client.get().exchangeToFlux(res -> res.bodyToFlux(User.class))
      .onErrorResume(Flux::error);
}

此代码直接获取用户信息,未指定状态码。

3.4. 修改响应体

修改响应体的示例:

@GetMapping("/user/exchange-alter/{id}")
Mono<User> retrieveOneUserWithExchange(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .exchangeToMono(res -> res.bodyToMono(User.class))
      .map(user -> {
          user.setName(user.getName().toUpperCase());
          user.setId(user.getId() + 100);
          return user;
      });
}

将响应体映射到 POJO 后,修改 id(加 100)和 name(转为大写)。

⚠️ retrieve() 方法同样可修改响应体。

3.5. 提取响应头

提取响应头的示例:

@GetMapping("/user/exchange-header/{id}")
Mono<User> retrieveUsersWithExchangeAndHeader(@PathVariable int id) {
  return client.get()
    .uri("/{id}", id)
    .exchangeToMono(res -> {
        if (res.statusCode().is2xxSuccessful()) {
            logger.info("Status code: " + res.headers().asHttpHeaders());
            logger.info("Content-type" + res.headers().contentType());
            return res.bodyToMono(User.class);
        } else if (res.statusCode().is4xxClientError()) {
            return Mono.error(new RuntimeException("Client Error: can't fetch user"));
        } else if (res.statusCode().is5xxServerError()) {
            return Mono.error(new RuntimeException("Server Error: can't fetch user"));
        } else {
            return res.createError();
        }
    });
}

记录 HTTP 头和内容类型到控制台。与需要返回 ResponseEntity 才能访问头和状态码的 retrieve() 不同,exchangeToMono() 因返回 ClientResponse 而可直接访问

4. retrieve() 方法详解

retrieve() 方法简化了从 HTTP 请求中提取响应体的过程。它返回 ResponseSpec,允许我们指定如何处理响应体,无需访问完整 ClientResponse。

ClientResponse 包含状态码、头和响应体,而 ResponseSpec 仅包含响应体。

4.1. 发射 Mono

获取 HTTP 响应体的示例代码:

@GetMapping("/user/{id}")
Mono<User> retrieveOneUser(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .retrieve()
      .bodyToMono(User.class)
      .onErrorResume(Mono::error);
}

通过向 /users 接口发送带特定 id 的 HTTP 请求获取 JSON,将响应体映射为 User 对象。

4.2. 发射 Flux

向 /users 接口发送 GET 请求的示例:

@GetMapping("/users")
Flux<User> retrieveAllUsers() {
    return client.get()
      .retrieve()
      .bodyToFlux(User.class)
      .onResumeError(Flux::error);
}

方法将 HTTP 响应映射为 POJO 类,发射 User 对象的 Flux。

4.3. 返回 ResponseEntity

使用 retrieve() 访问状态和头时,可返回 ResponseEntity:

@GetMapping("/user-id/{id}")
Mono<ResponseEntity<User>> retrieveOneUserWithResponseEntity(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .toEntity(User.class)
      .onErrorResume(Mono::error);
}

toEntity() 方法返回的响应包含 HTTP 头、状态码和响应体。

4.4. 使用 onStatus() 处理自定义错误

当出现 400 或 500 HTTP 错误时,默认返回 WebClientResponseException。但可通过 onStatus() 处理器自定义异常:

@GetMapping("/user-status/{id}")
Mono<User> retrieveOneUserAndHandleErrorBasedOnStatus(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .retrieve()
      .onStatus(HttpStatusCode::is4xxClientError, 
        response -> Mono.error(new RuntimeException("Client Error: can't fetch user")))
      .onStatus(HttpStatusCode::is5xxServerError, 
        response -> Mono.error(new RuntimeException("Server Error: can't fetch user")))
      .bodyToMono(User.class);
}

检查 HTTP 状态码,使用 onStatus() 处理器定义自定义错误响应。

5. 性能对比

使用 JMH (Java Microbenchmark Harness) 对比 retrieve() 和 exchangeToFlux() 的执行时间:

创建 RetrieveAndExchangeBenchmarkTest 类:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 10, timeUnit = TimeUnit.MICROSECONDS)
@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.MICROSECONDS)
public class RetrieveAndExchangeBenchmarkTest {
  
    private WebClient client;

    @Setup
    public void setup() {
        this.client = WebClient.create("https://jsonplaceholder.typicode.com/users");
    }
}

设置基准模式为 AverageTime,定义迭代次数和运行时间。使用 @Setup 注解在每次基准测试前创建 WebClient 实例。

使用 retrieve() 获取用户集合的基准方法:

@Benchmark
Flux<User> retrieveManyUserUsingRetrieveMethod() {
    return client.get()
      .retrieve()
      .bodyToFlux(User.class)
      .onErrorResume(Flux::error);;
}

使用 exchangeToFlux() 发射 User 对象 Flux 的方法:

@Benchmark
Flux<User> retrieveManyUserUsingExchangeToFlux() {
    return client.get()
      .exchangeToFlux(res -> res.bodyToFlux(User.class))
      .onErrorResume(Flux::error);
}

基准测试结果:

Benchmark                             Mode  Cnt   Score    Error  Units
retrieveManyUserUsingExchangeToFlux   avgt   15  ≈ 10⁻⁴            s/op
retrieveManyUserUsingRetrieveMethod   avgt   15  ≈ 10⁻³            s/op

两种方法均表现高效,但 exchangeToFlux() 在获取用户集合时略快于 retrieve()。

6. 核心差异与相似点

retrieve() 和 exchangeToMono()/exchangeToFlux() 均可用于执行 HTTP 请求并提取响应。

retrieve() 仅允许消费 HTTP 体并发射 Mono/Flux,因其返回 ResponseSpec。若需访问状态码和头,可结合 ResponseEntity 使用。它还支持通过 onStatus() 处理器基于 HTTP 状态码报告错误。

exchangeToMono()/exchangeToFlux() 允许消费 HTTP 响应并直接访问头和状态码,因其返回 ClientResponse。它们提供更精细的错误处理控制,可根据 HTTP 状态码解码响应。

若仅需消费响应体,建议使用 retrieve()
若需更精细的响应控制,选择 exchangeToMono() 或 exchangeToFlux()

7. 总结

本文学习了使用 retrieve()、exchangeToMono() 和 exchangeToFlux() 处理 HTTP 响应,并将响应映射到 POJO 类。同时对比了 retrieve() 和 exchangeToFlux() 的性能。

retrieve() 适用于仅需消费响应体、无需访问状态码或头的场景。它通过返回 ResponseSpec 简化了响应体处理流程。

完整示例代码可在 GitHub 获取。


原始标题:Spring WebClient exchange() vs retrieve()