Spring WebClient 中的 exchange() 和 retrieve() 方法
1、概览
WebClient
是一个简化 HTTP 请求执行过程的接口。与 RestTemplate
不同,它是一个响应式非阻塞客户端,可以消费和操作 HTTP 响应。虽然它被设计为非阻塞型,但也可用于阻塞型场景。
本文将带你了解 WebClient
接口中的关键方法,包括 retrieve()
、exchangeToMono()
和 exchangeToFlux()
,以及它们之间的差异。
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
类的实例。
最后,用 base URL 创建一个 WebClient
实例:
WebClient client = WebClient.create("https://jsonplaceholder.typicode.com/users");
如上,定义了 HTTP 请求的基 base URL。
3、exchange() 方法
exchange()
方法直接返回 ClientResponse
,从而可以直接访问 HTTP 状态码、Header 和响应体。简单地说,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()
方法将响应体映射到 Flux<User>
对象,并在请求失败时返回自定义错误信息。
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 并将名称大写来更改响应体。
注意,还可以使用 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 头信息和内容类型(Content Type)记录到控制台。与需要返回 ResponseEntity
才能访问头和响应码的 retrieve()
方法不同,exchangeToMono()
可以直接访问,因为它返回 ClientResponse
。
4、retrieve() 方法
retrieve()
方法简化了从 HTTP 请求中提取响应体的过程。它返回 ResponseSpec
,允许指定如何处理响应体,而无需访问完整的 ClientResponse
。
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);
}
如上,通过 HTTP 调用 /users
端点的特定 id
从 base URL 获取 JSON。然后,将响应体映射到 User
对象。
4.2、发送 Flux
向 /users
端点发起 GET 请求的示例如下:
@GetMapping("/users")
Flux<User> retrieveAllUsers() {
return client.get()
.retrieve()
.bodyToFlux(User.class)
.onResumeError(Flux::error);
}
如上,该方法在将 HTTP 响应映射到 POJO 类时,会发送一个 Flux<User>
对象。
4.3、返回 ResponseEntity
如果打算使用 retrieve()
方法访问响应状态码和 Header,可以返回 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() Handler 自定义 Error
此外,出现 400
或 500
HTTP 错误时,默认情况下会返回 WebClientResponseException
Error。不过,我们可以使用 onStatus()
Handler 自定义异常,以给出自定义错误响应:
@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()
Handler 定义自定义错误响应。
5、性能比较
接下来,使用 Java Microbench Harness (JMH) 编写一个性能测试,比较 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),即测量测试执行的平均时间。此外,还定义了迭代次数和每次迭代的运行时间。
接下来,创建一个 WebClient
实例,并使用 @Setup
注解使其在每次基准测试前运行。
编写一个基准方法,使用 retrieve()
方法检索用户集合:
@Benchmark
Flux<User> retrieveManyUserUsingRetrieveMethod() {
return client.get()
.retrieve()
.bodyToFlux(User.class)
.onErrorResume(Flux::error);;
}
最后,使用 exchangeToFlux()
方法定义一个发送 Flux<User>
对象方法:
@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 请求并提取 HTTP 响应。
retrieve()
方法只允许我们消费 HTTP Body 并发出 Mono
或 Flux
,因为它会返回 ResponseSpec
。但是,如果我们想访问状态码和 Header,可以使用带有 ResponseEntity
的 retrieve()
方法。此外,它还允许我们使用 onStatus()
Handler 根据 HTTP 状态码报告错误。
与 retrieve()
方法不同,exchangeToMono()
和 exchnageToFlux()
允许我们消费 HTTP 响应,并直接访问 Header 和响应状态码,因为它们会返回 ClientResponse
。此外,它们还提供了更多的错误处理控制,因为我们可以根据 HTTP 状态码对响应进行解码。
如果只想使用响应体,建议使用 retrieve()
方法。如果需要对响应进行更多控制,那么 exchangeToMono()
或 exchangeToFlux()
可能是更好的选择。
7、总结
本文介绍了如何使用 retrieve()
、exchangeToMono()
和 exchangeToFlux()
方法处理 HTTP 响应,以及如何将响应映射到 POJO 类。最后还比较了 retrieve()
和 exchangeToFlux()
方法的性能。
retrieve()
方法适用于只需要消费响应体,而不需要访问状态码或 Header 的场景。它通过返回 ResponseSpec
简化了过程,而 ResponseSpec
提供了一种直接处理响应体的方法。
Ref:https://www.baeldung.com/spsring-webclient-exchange-vs-retrieve