Appearance
WebClient 详解:响应式 HTTP 客户端
WebClient 是 Spring 5 引入的响应式、非阻塞 HTTP 客户端,是 RestTemplate 的现代替代品。它基于 Reactor 构建,天然支持异步、流式处理,是 Spring WebFlux 生态的核心组件。
一、为什么需要 WebClient?
RestTemplate 从 Spring 3 开始服役了十几年,但它有一个根本缺陷:阻塞式 IO。每个请求占用一个线程,等待响应期间线程什么也不做。高并发下线程池很快打满,资源利用率极低。
RestTemplate(阻塞式):
请求线程 → 发送 HTTP → 等待响应…… → 收到响应 → 返回
↑
线程一直阻塞在这里,什么都不做
1000 个并发 = 1000 个线程在等
WebClient(非阻塞式):
请求线程 → 发送 HTTP → 立即返回 Mono/Flux → 线程释放
│
└── 响应到达 → 事件通知 → 回调处理
1000 个并发 = 只需要几十个线程核心优势:
| 特性 | RestTemplate | WebClient |
|---|---|---|
| IO 模型 | 阻塞 | 非阻塞 |
| 返回类型 | 同步对象 | Mono/Flux |
| 线程模型 | 一个请求一个线程 | 少量线程处理大量请求 |
| 流式处理 | 不支持 | 原生支持 SSE、流式上传/下载 |
| 函数式风格 | 不支持 | 链式调用、过滤器、拦截器 |
| 状态 | 维护模式(不推荐新项目) | 活跃开发 |
| Spring Boot 版本 | 2.x 可用,3.x 需手动 | 所有版本推荐 |
一句话: RestTemplate 是用线程换简单,WebClient 是用事件驱动换性能。新项目统一用 WebClient。
1.1 版本兼容性说明
经常有人问"Spring Boot 3 只能用 OpenFeign 不能用 WebClient?"——这是对版本号的误解。
| Spring 版本 | Spring Boot 版本 | RestTemplate | WebClient | OpenFeign |
|---|---|---|---|---|
| Spring 4.x | Boot 1.x | ✅ | ❌ | ❌(只有 Netflix Feign) |
| Spring 5.x | Boot 2.x | ✅ | ✅(新生) | ✅ |
| Spring 6.x | Boot 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,异步用 WebClientSpring Boot 3.x 中 RestTemplate 虽然没有删除,但已进入维护模式(不再添加新功能),新项目应避免使用。
二、核心概念:响应式编程基础
WebClient 返回的是 Mono 和 Flux,不是普通的 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 全方位对比
| 维度 | OpenFeign | WebClient |
|---|---|---|
| 编程风格 | 声明式(接口 + 注解) | 编程式 + 函数式(链式调用) |
| IO 模型 | 阻塞(默认基于 HttpURLConnection) | 非阻塞(基于 Reactor Netty) |
| 异步支持 | 需配合 CompletableFuture | 原生 Mono/Flux 响应式 |
| 流式处理 | 不支持 | 原生支持 SSE、Stream |
| 代码量 | 少(只定义接口) | 中(需写构建逻辑) |
| 学习成本 | 低(像写 Controller) | 中(需理解响应式编程) |
| 负载均衡 | 内置 Ribbon/LoadBalancer | 需 @LoadBalanced + 手动配置 |
| 熔断降级 | 内置 Sentinel/Resilience4j 支持 | 需手动集成 |
| 超时配置 | 配置文件统一管理 | 编程式精细控制 |
| 重试机制 | 内置重试(配置即可) | 需手动编写 retry() |
| 拦截器 | RequestInterceptor | ExchangeFilterFunction |
| 连接池 | 依赖底层 HTTP 客户端 | Reactor Netty 连接池 |
| Spring 集成 | 深度集成(开箱即用) | 需手动配置 |
| 适用场景 | 同步微服务间调用 | 响应式架构 / 高并发异步 |
| 底层可替换 | 可切换 HttpClient/OkHttp | 可切换 Jetty/Undertow |
9.4 同步 vs 异步:核心差异
OpenFeign(同步阻塞):
请求线程 ─── 发送 HTTP 请求 ─── 等待响应 ─── 收到响应 ─── 返回结果
└──────────────── 线程全程阻塞等待 ────────────────┘
一个请求占用一个线程,请求多了线程池耗尽
WebClient(异步非阻塞):
请求线程 ─── 发送 HTTP 请求 ─── 立即返回 Mono ─── 线程释放去处理其他请求
│
响应到达时回调
│
线程池中的线程处理响应
少量线程处理大量请求,线程不会阻塞等待9.5 性能对比
| 场景 | OpenFeign | WebClient |
|---|---|---|
| 单次请求延迟 | ~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(非阻塞,原生异步);两者可以共存。
