Skip to content

WebClient 详解:响应式 HTTP 客户端

  WebClient 是 Spring 5 引入的响应式、非阻塞 HTTP 客户端,是 RestTemplate 的现代替代品。它基于 Reactor 构建,天然支持异步、流式处理,是 Spring WebFlux 生态的核心组件。

一、为什么需要 WebClient?

  RestTemplate 从 Spring 3 开始服役了十几年,但它有一个根本缺陷:阻塞式 IO。每个请求占用一个线程,等待响应期间线程什么也不做。高并发下线程池很快打满,资源利用率极低。

RestTemplate(阻塞式):
  请求线程 → 发送 HTTP → 等待响应…… → 收到响应 → 返回

                     线程一直阻塞在这里,什么都不做
  1000 个并发 = 1000 个线程在等


WebClient(非阻塞式):
  请求线程 → 发送 HTTP → 立即返回 Mono/Flux → 线程释放

                              └── 响应到达 → 事件通知 → 回调处理
  1000 个并发 = 只需要几十个线程

核心优势:

特性RestTemplateWebClient
IO 模型阻塞非阻塞
返回类型同步对象Mono/Flux
线程模型一个请求一个线程少量线程处理大量请求
流式处理不支持原生支持 SSE、流式上传/下载
函数式风格不支持链式调用、过滤器、拦截器
状态维护模式(不推荐新项目)活跃开发
Spring Boot 版本2.x 可用,3.x 需手动所有版本推荐

一句话: RestTemplate 是用线程换简单,WebClient 是用事件驱动换性能。新项目统一用 WebClient。

1.1 版本兼容性说明

  经常有人问"Spring Boot 3 只能用 OpenFeign 不能用 WebClient?"——这是对版本号的误解。

Spring 版本Spring Boot 版本RestTemplateWebClientOpenFeign
Spring 4.xBoot 1.x❌(只有 Netflix Feign)
Spring 5.xBoot 2.x✅(新生)
Spring 6.xBoot 3.x⚠️ 维护模式✅(推荐)

关键结论:Spring Boot 3.x 中 WebClient 和 OpenFeign 都可以用,且 WebClient 是官方推荐。被淘汰的是 RestTemplate,不是 WebClient。

版本号混淆澄清:

  • Spring 3(2009 年)= 上古版本,早就淘汰了,那时候连 RestTemplate 都还没成熟
  • Spring Boot 3(2022 年)= 当前最新版本,基于 Spring Framework 6,WebClient 完全可用
  • 两者差了 13 年,不是同一个东西

Spring Boot 3.x 中如何选择:

Spring Boot 3.x 项目:

  ┌─ 微服务间同步调用 ───→ OpenFeign(声明式,代码量最少)

  ├─ 响应式架构(WebFlux)──→ WebClient(唯一选择,原生异步)

  ├─ 高并发异步调用 ───→ WebClient(非阻塞,省线程)

  └─ 两者共存 ───→ 同步用 Feign,异步用 WebClient

Spring Boot 3.x 中 RestTemplate 虽然没有删除,但已进入维护模式(不再添加新功能),新项目应避免使用。

二、核心概念:响应式编程基础

  WebClient 返回的是 MonoFlux,不是普通的 Java 对象。先理解这两个核心类型。

Mono<T>      → 0 或 1 个元素(单个请求的响应)
Flux<T>      → 0 到 N 个元素(流式数据、SSE)

Mono<String>  = "一个字符串,可能为空"
Flux<User>    = "一个用户流,可能 0 个、1 个或多个"

关键特性:

java
// 惰性执行:不调用 subscribe() / block(),请求不会发出
Mono<String> mono = webClient.get()
    .uri("/api/user/1")
    .retrieve()
    .bodyToMono(String.class);
// ↑ 到这里,HTTP 请求还没发出去!只是声明了"我要做什么"

// 触发执行的方式:
mono.subscribe();           // 异步订阅,不阻塞
String result = mono.block();  // 同步阻塞等待(仅测试/演示用)

subscribe() 不是阻塞调用:

java
webClient.get()
    .uri("/api/user/1")
    .retrieve()
    .bodyToMono(User.class)
    .subscribe(
        user -> log.info("成功:{}", user),      // onNext
        error -> log.error("失败", error),       // onError
        () -> log.info("完成")                    // onComplete
    );
// 主线程立即继续执行,不等待响应

三、基本使用

3.1 创建 WebClient

java
// 方式一:最简单
WebClient client = WebClient.create();

// 方式二:指定基础 URL
WebClient client = WebClient.create("http://localhost:8080");

// 方式三:Builder 模式(推荐,可配置全局过滤器、编解码器)
WebClient client = WebClient.builder()
    .baseUrl("http://order-service")
    .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer token")
    .defaultCookie("sessionId", "abc123")
    .codecs(configurer -> configurer
        .defaultCodecs()
        .maxInMemorySize(16 * 1024 * 1024)  // 缓冲区 16MB
    )
    .build();

3.2 GET 请求

java
// 同步阻塞(仅测试用)
User user = webClient.get()
    .uri("/api/user/{id}", 1)
    .retrieve()
    .bodyToMono(User.class)
    .block();  // 阻塞等待,生产环境不要用

// 异步非阻塞(推荐)
Mono<User> userMono = webClient.get()
    .uri(uriBuilder -> uriBuilder
        .path("/api/user/{id}")
        .queryParam("detail", true)
        .build(1))
    .retrieve()
    .bodyToMono(User.class);

// 链式处理
webClient.get()
    .uri("/api/user/1")
    .retrieve()
    .bodyToMono(User.class)
    .map(User::getName)           // 只取用户名
    .flatMap(this::sendEmail)     // 异步发邮件
    .subscribe();

3.3 POST 请求

java
OrderDTO dto = new OrderDTO("P001", 2);

Order order = webClient.post()
    .uri("/api/order/create")
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue(dto)               // 自动序列化为 JSON
    .retrieve()
    .bodyToMono(Order.class)
    .block();

3.4 PUT / DELETE

java
// PUT
webClient.put()
    .uri("/api/user/{id}", 1)
    .bodyValue(updatedUser)
    .retrieve()
    .bodyToMono(Void.class);

// DELETE
webClient.delete()
    .uri("/api/user/{id}", 1)
    .retrieve()
    .bodyToMono(Void.class);

3.5 表单提交

java
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("username", "admin");
form.add("password", "123456");

webClient.post()
    .uri("/api/login")
    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
    .bodyValue(form)
    .retrieve()
    .bodyToMono(String.class);

3.6 文件上传

java
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("file", new FileSystemResource("D:/report.pdf"));
builder.part("description", "月度报告");

webClient.post()
    .uri("/api/upload")
    .contentType(MediaType.MULTIPART_FORM_DATA)
    .bodyValue(builder.build())
    .retrieve()
    .bodyToMono(String.class);

四、错误处理

4.1 按 HTTP 状态码处理

java
User user = webClient.get()
    .uri("/api/user/{id}", id)
    .retrieve()
    .onStatus(
        status -> status == HttpStatus.NOT_FOUND,
        response -> Mono.error(new UserNotFoundException("用户不存在"))
    )
    .onStatus(
        HttpStatusCode::is5xxServerError,
        response -> Mono.error(new ServiceUnavailableException("服务不可用"))
    )
    .bodyToMono(User.class)
    .block();

4.2 统一错误处理

java
// 全局过滤器统一处理(推荐)
@Bean
public WebClient webClient() {
    return WebClient.builder()
        .filter((request, next) -> next.exchange(request)
            .flatMap(response -> {
                if (response.statusCode().is4xxClientError()) {
                    return response.bodyToMono(String.class)
                        .flatMap(body -> Mono.error(
                            new BusinessException("客户端错误:" + body)));
                }
                if (response.statusCode().is5xxServerError()) {
                    return Mono.error(
                        new ServiceUnavailableException("服务端错误"));
                }
                return Mono.just(response);
            }))
        .build();
}

4.3 exchangeToMono — 更灵活的处理

java
// retrieve() 会自动处理 4xx/5xx 为 WebClientResponseException
// exchangeToMono() 给你完整的 ClientResponse,自己决定如何处理

User user = webClient.get()
    .uri("/api/user/{id}", id)
    .exchangeToMono(response -> {
        if (response.statusCode().is2xxSuccessful()) {
            return response.bodyToMono(User.class);
        } else {
            return response.bodyToMono(ErrorResponse.class)
                .flatMap(error -> Mono.error(
                    new BusinessException(error.getMessage())));
        }
    })
    .block();

五、重试机制

java
User user = webClient.get()
    .uri("/api/user/{id}", id)
    .retrieve()
    .bodyToMono(User.class)
    .retryWhen(Retry.backoff(3, Duration.ofMillis(100))
        .maxBackoff(Duration.ofSeconds(3))
        .filter(throwable -> throwable instanceof ConnectException)  // 只重试连接异常
        .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) ->
            new ServiceUnavailableException("重试 3 次后仍失败")
        ))
    .block();

重试策略选择:

策略方法适用场景
固定延迟Retry.fixedDelay(3, Duration.ofSeconds(1))简单重试
指数退避Retry.backoff(3, Duration.ofMillis(100))网络抖动(推荐)
条件重试.filter(e -> e instanceof ConnectException)只重试特定异常

六、超时配置

java
// 方式一:全局配置
@Bean
public WebClient webClient() {
    HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)  // 连接超时
        .responseTimeout(Duration.ofSeconds(5));              // 响应超时

    return WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
}

// 方式二:单次请求配置
User user = webClient.get()
    .uri("/api/user/{id}", id)
    .retrieve()
    .bodyToMono(User.class)
    .timeout(Duration.ofSeconds(3))  // 3 秒超时
    .block();

七、请求拦截器(过滤器)

java
// WebClient 使用 ExchangeFilterFunction 实现拦截器
@Bean
public WebClient webClient() {
    return WebClient.builder()
        .filter(authFilter())        // 认证过滤器
        .filter(loggingFilter())     // 日志过滤器
        .filter(traceFilter())       // 链路追踪过滤器
        .build();
}

// 认证过滤器
private ExchangeFilterFunction authFilter() {
    return (request, next) -> {
        String token = RequestContextHolder.currentRequestAttributes()
            .getRequest().getHeader("Authorization");
        return next.exchange(
            ClientRequest.from(request)
                .header("Authorization", token)
                .build()
        );
    };
}

// 日志过滤器
private ExchangeFilterFunction loggingFilter() {
    return (request, next) -> {
        log.info("请求: {} {}", request.method(), request.url());
        return next.exchange(request)
            .doOnNext(response ->
                log.info("响应: {} {} 耗时: {}ms",
                    response.statusCode(), request.url(), /* 耗时 */));
    };
}

八、与 Spring Cloud LoadBalancer 集成

xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
java
// 方式一:声明式(推荐)
@Bean
@LoadBalanced  // 开启负载均衡
public WebClient.Builder loadBalancedWebClientBuilder() {
    return WebClient.builder();
}

@Service
public class OrderService {
    @Autowired
    private WebClient.Builder webClientBuilder;

    public User getUser(Long id) {
        return webClientBuilder.build()
            .get()
            .uri("http://user-service/api/user/{id}", id)  // 用服务名代替 IP
            .retrieve()
            .bodyToMono(User.class)
            .block();
    }
}

// 方式二:过滤器方式
@Bean
public WebClient webClient(ReactorLoadBalancerExchangeFilterFunction lbFunction) {
    return WebClient.builder()
        .filter(lbFunction)
        .build();
}

// 使用
webClient.get()
    .uri("http://user-service/api/user/{id}", id)  // 自动负载均衡
    .retrieve()
    .bodyToMono(User.class);

九、OpenFeign vs WebClient 深度对比

  OpenFeign 和 WebClient 都是 Spring Cloud 生态中的 HTTP 客户端,但设计哲学完全不同。理解它们的本质区别,才能做出正确的技术选型。

9.1 设计哲学

OpenFeign:声明式(Declarative)
  "我要调用 order-service 的 createOrder 方法"
  你定义接口 → 框架生成实现 → 你调用接口方法
  像调用本地方法一样调用远程服务

WebClient:编程式(Programmatic)
  "我要对 http://order-service/api/orders 发一个 POST 请求"
  你构建请求 → 发送请求 → 处理响应
  每一步都由你显式控制

9.2 代码对比

java
// ===== OpenFeign:声明式 =====
@FeignClient(name = "order-service")
public interface OrderClient {
    @PostMapping("/api/orders")
    Order createOrder(@RequestBody OrderDTO dto);

    @GetMapping("/api/orders/{id}")
    Order getOrder(@PathVariable Long id);
}

// 使用:像调用本地方法
@Autowired
private OrderClient orderClient;
Order order = orderClient.createOrder(dto);


// ===== WebClient:编程式 =====
@Autowired
private WebClient.Builder webClientBuilder;

public Order createOrder(OrderDTO dto) {
    return webClientBuilder.build()
        .post()
        .uri("http://order-service/api/orders")
        .bodyValue(dto)
        .retrieve()
        .bodyToMono(Order.class)
        .block();  // 同步等待
}

9.3 全方位对比

维度OpenFeignWebClient
编程风格声明式(接口 + 注解)编程式 + 函数式(链式调用)
IO 模型阻塞(默认基于 HttpURLConnection)非阻塞(基于 Reactor Netty)
异步支持需配合 CompletableFuture原生 Mono/Flux 响应式
流式处理不支持原生支持 SSE、Stream
代码量少(只定义接口)中(需写构建逻辑)
学习成本低(像写 Controller)中(需理解响应式编程)
负载均衡内置 Ribbon/LoadBalancer需 @LoadBalanced + 手动配置
熔断降级内置 Sentinel/Resilience4j 支持需手动集成
超时配置配置文件统一管理编程式精细控制
重试机制内置重试(配置即可)需手动编写 retry()
拦截器RequestInterceptorExchangeFilterFunction
连接池依赖底层 HTTP 客户端Reactor Netty 连接池
Spring 集成深度集成(开箱即用)需手动配置
适用场景同步微服务间调用响应式架构 / 高并发异步
底层可替换可切换 HttpClient/OkHttp可切换 Jetty/Undertow

9.4 同步 vs 异步:核心差异

OpenFeign(同步阻塞):
  请求线程 ─── 发送 HTTP 请求 ─── 等待响应 ─── 收到响应 ─── 返回结果
             └──────────────── 线程全程阻塞等待 ────────────────┘
  一个请求占用一个线程,请求多了线程池耗尽

WebClient(异步非阻塞):
  请求线程 ─── 发送 HTTP 请求 ─── 立即返回 Mono ─── 线程释放去处理其他请求

                                   响应到达时回调

                                   线程池中的线程处理响应
  少量线程处理大量请求,线程不会阻塞等待

9.5 性能对比

场景OpenFeignWebClient
单次请求延迟~5ms~3ms
100 并发(Tomcat 200 线程)约 2000 QPS约 5000 QPS
1000 并发线程池耗尽,排队稳定运行,无排队
内存占用较高(线程栈)低(事件循环)

9.6 选择建议

                    ┌─ 项目是响应式架构(WebFlux)? ──┐
                    │ 是                              │ 否
                    ▼                                 ▼
                WebClient                   ┌─ 需要流式处理 / SSE? ──┐
              (唯一选择)                   │ 是                      │ 否
                                            ▼                         ▼
                                        WebClient             ┌─ 追求代码简洁? ──┐
                                                              │ 是              │ 否
                                                              ▼                 ▼
                                                          OpenFeign         WebClient
                                                         (推荐)          (更灵活)

一句话总结:

场景推荐
微服务间同步调用OpenFeign(声明式,代码量最少,开箱即用)
响应式全栈(WebFlux)WebClient(唯一选择,原生异步)
高并发异步调用WebClient(非阻塞,少量线程处理大量请求)
流式数据传输(SSE)WebClient(原生 Flux 支持)
需要精细控制WebClient(编程式,灵活控制每一步)
快速开发、代码量少OpenFeign(声明式,接口即文档)
两者共存可以,同步用 Feign,异步用 WebClient

9.7 实战:OpenFeign + WebClient 混用

  真实项目中两者可以共存,各取所长。以下是一个 order-service 同时使用两种客户端的例子:

java
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    // OpenFeign:同步调用,代码简洁
    @Autowired
    private UserClient userClient;       // 查用户信息
    @Autowired
    private InventoryClient inventoryClient; // 查库存

    // WebClient:异步调用,非阻塞
    @Autowired
    private WebClient webClient;         // 异步记日志、发通知

    @PostMapping
    public Order createOrder(@RequestBody OrderDTO dto) {

        // ① OpenFeign 同步调用:查用户信息(必须等结果)
        User user = userClient.getUser(dto.getUserId());
        if (user == null) {
            throw new RuntimeException("用户不存在");
        }

        // ② OpenFeign 同步调用:扣库存(必须等结果)
        boolean deducted = inventoryClient.deduct(dto.getProductId(), dto.getQuantity());
        if (!deducted) {
            throw new RuntimeException("库存不足");
        }

        // ③ WebClient 异步调用:记操作日志(不需要等结果,发完就返回)
        webClient.post()
            .uri("http://log-service/api/logs")
            .bodyValue(Map.of("action", "createOrder", "userId", dto.getUserId()))
            .retrieve()
            .toBodilessEntity()
            .subscribe();  // 异步订阅,不阻塞主流程

        // ④ WebClient 并行调用:同时查优惠券和积分(两个异步请求并行执行)
        Mono<Coupon> couponMono = webClient.get()
            .uri("http://coupon-service/api/coupons/" + dto.getUserId())
            .retrieve()
            .bodyToMono(Coupon.class);

        Mono<Points> pointsMono = webClient.get()
            .uri("http://points-service/api/points/" + dto.getUserId())
            .retrieve()
            .bodyToMono(Points.class);

        // Mono.zip 并行执行,汇总结果
        Order order = Mono.zip(couponMono, pointsMono)
            .map(tuple -> {
                Coupon coupon = tuple.getT1();
                Points points = tuple.getT2();
                return buildOrder(dto, user, coupon, points);
            })
            .block();  // 最终还是要等结果,但耗时 = max(两个请求),不是 sum

        return order;
    }
}

混用原则:

需要等待结果才能继续 → OpenFeign(同步,代码简洁)
  - 查用户信息、扣库存、创建订单

不需要等待结果 → WebClient 异步(不阻塞)
  - 记日志、发通知、埋点

需要并行调用多个服务 → WebClient + Mono.zip(耗时取 max 而非 sum)
  - 查优惠券 + 查积分 + 查运费

已经在响应式链路中 → 统一用 WebClient(全链路非阻塞)
  - WebFlux Controller → WebClient → R2DBC

十、最佳实践

1. 复用 WebClient 实例:

java
// ✅ 正确:一个应用一个 WebClient 实例,全局复用
@Bean
public WebClient webClient() {
    return WebClient.builder().baseUrl("http://api.example.com").build();
}

// ❌ 错误:每次请求创建新实例,浪费连接池
public User getUser() {
    WebClient client = WebClient.create();  // 不要这样做
    return client.get()...block();
}

2. 不要在生产环境使用 block():

java
// ❌ 错误:block() 把非阻塞变成阻塞,失去 WebClient 的优势
User user = webClient.get()...bodyToMono(User.class).block();

// ✅ 正确:保持响应式链
Mono<User> userMono = webClient.get()...bodyToMono(User.class);
return userMono.flatMap(this::processUser);

3. 配置合理的缓冲区大小:

java
// 默认 256KB,大数据响应需要调整
WebClient.builder()
    .codecs(configurer -> configurer
        .defaultCodecs()
        .maxInMemorySize(16 * 1024 * 1024)  // 16MB
    )
    .build();

4. 统一错误处理用过滤器,不要每次请求都写 onStatus:

java
// ✅ 过滤器统一处理
@Bean
public WebClient webClient() {
    return WebClient.builder()
        .filter(errorHandlingFilter())
        .build();
}

5. 连接池配置:

yaml
spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true
# WebClient 底层使用 Reactor Netty,连接池默认已配置
# 如需调整:
# 在 HttpClient 创建时配置连接池参数

十一、面试要点

Q1:WebClient 和 RestTemplate 的区别?

  RestTemplate 是阻塞式 HTTP 客户端,一个请求一个线程;WebClient 是非阻塞式,基于 Reactor Netty,少量线程处理大量请求。RestTemplate 已进入维护模式,Spring 官方推荐 WebClient。

Q2:WebClient 的底层实现?

  默认使用 Reactor Netty(基于 Netty),支持 HTTP/1.1。也可通过 ClientHttpConnector 切换到 Jetty 或 Apache HttpClient。

Q3:Mono 和 Flux 的区别?

  Mono 表示 0 或 1 个元素(单个响应),Flux 表示 0 到 N 个元素(流式数据)。WebClient 的 bodyToMono() 用于单个对象,bodyToFlux() 用于数组/流。

Q4:WebClient 如何实现超时?

  两种方式:全局配置 HttpClient.responseTimeout(),或单次请求 .timeout(Duration.ofSeconds(3))

Q5:WebClient 和 OpenFeign 怎么选?

  同步微服务调用选 OpenFeign(声明式,代码少);响应式架构选 WebClient(非阻塞,原生异步);两者可以共存。