1. 简介
本教程将探讨如何使用 WebClient
执行同步 HTTP 请求。尽管响应式编程日益普及,我们仍会分析阻塞请求适用的场景及其必要性。
2. Spring HTTP 客户端库概览
先快速梳理当前可用的客户端库,明确 WebClient
的定位:
✅ RestTemplate(Spring 3.0 引入):凭借简洁的模板方法 API 流行,但存在两个痛点
- 同步特性
- 方法重载过多导致高并发场景下的性能瓶颈
✅ WebClient(Spring 5.0 引入):作为高效的非阻塞响应式替代方案
- 属于响应式栈框架(Spring WebFlux)
- 通过流式 API 同时支持同步/异步通信
✅ RestClient(Spring Framework 6.1 引入):新增的 REST 调用选项
- 融合
WebClient
的流式 API 与RestTemplate
的基础设施(消息转换器、请求工厂等) - 专为同步请求优化
- 融合
⚠️ 关键结论:
- 需要异步/流式能力时优先选
WebClient
- 使用
WebClient
统一处理阻塞/非阻塞调用,避免混用不同客户端库
3. 阻塞 vs 非阻塞 API 调用
澄清易混淆的概念:
术语 | 含义 |
---|---|
同步调用 | 等待前一个请求完成才执行后续请求,结果同步返回 |
异步调用 | 非阻塞调用立即返回,等待响应期间可执行其他任务,结果异步通知 |
阻塞调用 | 调用线程暂停直到操作完成 |
非阻塞调用 | 调用立即返回,通过回调/事件通知结果 |
WebClient
的核心优势:通过block()
方法实现同步阻塞调用,底层仍保持响应式基础
4. 同步请求适用场景
尽管 WebClient
默认响应式,但以下场景适合同步调用:
快速验证阶段
- 测试/原型开发时需即时反馈,优先保证功能正确性
遗留系统改造
RestTemplate
已进入维护模式(Spring 5.0+)- 临时用
WebClient
阻塞调用替代,降低重构风险
同步工作流需求
- 顺序调用外部系统(如:需前序结果触发后续调用)
- 避免混用不同客户端库,保持代码一致性
❌ 重要提醒:
- 响应式栈中应尽量避免阻塞调用
- 同步/异步切换成本较低,优先考虑非阻塞方案
5. WebClient 同步调用实现
WebClient
返回 Reactor 的 Mono
(单值)或 Flux
(多值)类型。同步调用关键点:
// 阻塞调用核心方法
responseMono.block(); // 阻塞当前线程获取结果
block()
工作原理:
- 触发新的响应式流订阅
- 内部使用
CountDownLatch
等待流完成 - 将非阻塞操作转为阻塞调用,暂停调用线程直到结果返回
⚠️ 性能影响:
- 阻塞调用会占用线程资源
- 高并发场景可能成为性能瓶颈
6. 实战案例
构建 API 网关聚合客户(Customer)和计费(Billing)系统数据:
6.1. 基础实现
网关接口定义:
@GetMapping("/{id}")
CustomerInfo getCustomerInfo(@PathVariable("id") Long customerId) {
return customerInfoService.getCustomerInfo(customerId);
}
数据模型:
public class CustomerInfo {
private Long customerId;
private String customerName;
private Double balance;
// 标准getter/setter
}
后端系统模拟(添加2秒延迟模拟网络延迟):
// 客户系统
@GetMapping("/{id}")
Mono<Customer> getCustomer(@PathVariable("id") Long customerId) {
TimeUnit.SECONDS.sleep(2); // 模拟延迟
return Mono.just(customerService.getBy(customerId));
}
// 计费系统
@GetMapping("/{id}")
Mono<Billing> getBilling(@PathVariable("id") Long customerId) {
TimeUnit.SECONDS.sleep(2); // 模拟延迟
return Mono.just(billingService.getBy(customerId));
}
同步调用实现:
// 调用客户系统
Customer customer = webClient.get()
.uri("/customers/{id}", customerId)
.retrieve()
.onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
response -> response.bodyToMono(String.class)
.map(ApiGatewayException::new))
.bodyToMono(Customer.class)
.block(); // 阻塞获取结果
// 调用计费系统
Billing billing = webClient.get()
.uri("/billings/{id}", customerId)
.retrieve()
.onStatus(/* 同上错误处理 */)
.bodyToMono(Billing.class)
.block(); // 阻塞获取结果
// 组装结果
return new CustomerInfo(customer.getId(), customer.getName(), billing.getBalance());
6.2. 多调用优化
问题:顺序阻塞两个调用导致总耗时=4秒(2+2)
优化方案:使用 Mono.zip()
并行调用
private CustomerInfo getCustomerInfoOptimized(Long customerId) {
// 并行发起两个请求
Mono<Customer> customerMono = webClient.get()
.uri("/customers/{id}", customerId)
.retrieve()
.bodyToMono(Customer.class);
Mono<Billing> billingMono = webClient.get()
.uri("/billings/{id}", customerId)
.retrieve()
.bodyToMono(Billing.class);
// 合并结果(总耗时≈2秒)
return Mono.zip(customerMono, billingMono, (customer, billing) ->
new CustomerInfo(customer.getId(), customer.getName(), billing.getBalance())
).block(); // 阻塞等待合并结果
}
性能验证测试:
@Test
void testOptimizedBlockingCall() {
Long customerId = 10L;
// 设置超时为3秒(优化前需4秒,优化后需2秒)
assertTimeout(Duration.ofSeconds(3), () -> {
testClient.get()
.uri("/customer-info/{id}", customerId)
.exchange()
.expectStatus().isOk();
});
}
✅ 优化效果:
- 并行调用将总耗时从4秒降至2秒
- 即使使用阻塞调用,仍可通过响应式特性优化性能
7. 总结
WebClient
虽为响应式设计,但通过block()
可实现同步调用- 优势对比:
- vs
RestClient
:统一处理阻塞/非阻塞调用,避免库混用 - vs
RestTemplate
:更现代的API设计,支持异步扩展
- vs
- 关键实践:
- 优先使用
Mono.zip()
等操作符优化多调用场景 - 响应式栈中谨慎使用阻塞调用
- 同步/异步切换成本低,根据实际需求灵活选择
- 优先使用
完整源码见 GitHub