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 获取。