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 默认响应式,但以下场景适合同步调用:

  1. 快速验证阶段

    • 测试/原型开发时需即时反馈,优先保证功能正确性
  2. 遗留系统改造

    • RestTemplate 已进入维护模式(Spring 5.0+)
    • 临时用 WebClient 阻塞调用替代,降低重构风险
  3. 同步工作流需求

    • 顺序调用外部系统(如:需前序结果触发后续调用)
    • 避免混用不同客户端库,保持代码一致性

❌ 重要提醒:

  • 响应式栈中应尽量避免阻塞调用
  • 同步/异步切换成本较低,优先考虑非阻塞方案

5. WebClient 同步调用实现

WebClient 返回 Reactor 的 Mono(单值)或 Flux(多值)类型。同步调用关键点:

// 阻塞调用核心方法
responseMono.block();  // 阻塞当前线程获取结果

block() 工作原理:

  1. 触发新的响应式流订阅
  2. 内部使用 CountDownLatch 等待流完成
  3. 将非阻塞操作转为阻塞调用,暂停调用线程直到结果返回

⚠️ 性能影响:

  • 阻塞调用会占用线程资源
  • 高并发场景可能成为性能瓶颈

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设计,支持异步扩展
  • 关键实践:
    • 优先使用 Mono.zip() 等操作符优化多调用场景
    • 响应式栈中谨慎使用阻塞调用
    • 同步/异步切换成本低,根据实际需求灵活选择

完整源码见 GitHub


原始标题:Execute Synchronous Requests Using WebClient | Baeldung