Appearance
第63课:Spring WebFlux
🎯 学习目标
- 理解 WebFlux 的响应式、异步非阻塞模型,以及它和 Spring MVC 的区别。
- 掌握
Mono、Flux、WebFlux Controller、RouterFunction 和 WebClient 的基本使用。 - 能识别阻塞调用混入响应式链路、错误处理缺失、背压误解等常见问题。
- 能判断什么场景适合 WebFlux,什么场景继续用 Spring MVC 更合适。
- 理解响应式编程的收益来自端到端非阻塞,而不是简单把返回值改成
Mono。
📖 一、WebFlux 是什么
Spring MVC 是同步阻塞模型:
text
一个请求通常占用一个 Servlet 线程,直到处理完成。Spring WebFlux 是响应式 Web 框架:
text
少量事件循环线程处理大量连接。
IO 操作不阻塞线程。
结果准备好后通过回调/事件继续处理。它适合高并发 IO 密集型场景,例如网关、聚合 API、长连接、流式响应。
如果你的应用主要是阻塞 JDBC + 普通 CRUD,直接换 WebFlux 未必更快,甚至可能更复杂。
📖 二、Mono 和 Flux
Mono<T> 表示 0 或 1 个元素:
java
Mono<String> mono = Mono.just("hello");
Mono<String> empty = Mono.empty();
Mono<String> error = Mono.error(new RuntimeException("failed"));Flux<T> 表示 0 到 N 个元素:
java
Flux<String> names = Flux.just("Alice", "Bob", "Carol");
Flux<Integer> range = Flux.range(1, 10);常见操作:
java
Mono<User> user = userRepository.findById(id)
.filter(User::isEnabled)
.switchIfEmpty(Mono.error(new NotFoundException("user not found")));java
Flux<String> names = userRepository.findAll()
.filter(User::isEnabled)
.map(User::getName);响应式链路是声明式的,不调用订阅或框架订阅时不会真正执行。
📖 三、WebFlux Controller
java
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public Mono<UserResponse> getById(@PathVariable Long id) {
return userService.getById(id);
}
@GetMapping
public Flux<UserResponse> list() {
return userService.findAll();
}
@PostMapping
public Mono<UserResponse> create(@RequestBody Mono<UserCreateRequest> request) {
return request.flatMap(userService::create);
}
}Spring 会订阅返回的 Mono 或 Flux,并把结果写入响应。
📖 四、函数式路由
WebFlux 也支持函数式风格:
java
@Configuration
public class UserRouter {
@Bean
public RouterFunction<ServerResponse> routes(UserHandler handler) {
return RouterFunctions.route()
.GET("/api/users/{id}", handler::getById)
.POST("/api/users", handler::create)
.build();
}
}Handler:
java
@Component
public class UserHandler {
public Mono<ServerResponse> getById(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.getById(id)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
}函数式路由适合网关、轻量 API 和希望显式组合路由的场景。
📖 五、WebClient
WebClient 是响应式 HTTP 客户端。
java
WebClient client = WebClient.builder()
.baseUrl("https://api.example.com")
.build();GET:
java
Mono<UserResponse> user = client.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(UserResponse.class);错误处理:
java
Mono<UserResponse> user = client.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
response -> Mono.error(new BusinessException("USER_NOT_FOUND", "用户不存在")))
.onStatus(HttpStatusCode::is5xxServerError,
response -> Mono.error(new RuntimeException("remote server error")))
.bodyToMono(UserResponse.class)
.timeout(Duration.ofSeconds(3));不要在响应式链路中随意 .block()。
📖 六、阻塞调用处理
错误示例:
java
public Mono<UserResponse> getById(Long id) {
UserEntity entity = jdbcUserRepository.findById(id); // 阻塞 JDBC
return Mono.just(UserResponse.from(entity));
}这会阻塞事件循环线程。
如果必须调用阻塞代码,可以临时隔离到弹性线程池:
java
public Mono<UserResponse> getById(Long id) {
return Mono.fromCallable(() -> jdbcUserRepository.findById(id))
.subscribeOn(Schedulers.boundedElastic())
.map(UserResponse::from);
}但这只是兼容方案。真正的响应式收益来自 R2DBC、Reactive Redis、Reactive Mongo、WebClient 等端到端非阻塞。
📖 七、背压
背压是消费者告诉生产者“我处理不过来,请慢一点”的机制。
java
Flux.range(1, 1_000_000)
.onBackpressureBuffer(1000)
.map(this::process);注意:HTTP 请求/响应场景中,背压能力还受网络、客户端、服务器实现影响。不要以为用了 Flux 就自动解决所有流量压力。
⚠️ 八、常见陷阱
1. 在事件循环中 block
java
webClient.get().retrieve().bodyToMono(User.class).block();这会破坏非阻塞模型。
2. 阻塞数据库驱动混入 WebFlux
JDBC 是阻塞的。WebFlux + JDBC 通常得不到预期收益。
3. 错误处理缺失
响应式链中异常不会像同步代码那样直观,需要使用 onErrorResume、onStatus、doOnError 等处理。
4. 滥用响应式
普通 CRUD 后台系统用 Spring MVC 更简单。WebFlux 不是更高级的 MVC,而是不同编程模型。
🆚 九、Java vs C 对比
| 维度 | C 事件驱动服务 | WebFlux |
|---|---|---|
| IO 模型 | epoll/kqueue + 回调 | Reactor + Netty |
| 数据流 | 手写状态机 | Mono/Flux 操作链 |
| 背压 | 手动协议设计 | Reactive Streams |
| 错误传播 | 回调错误码 | 响应式错误信号 |
WebFlux 把事件驱动和响应式流封装成 Java API,但理解成本明显高于同步 MVC。
💡 十、最佳实践
- 只有明确需要高并发非阻塞 IO 时再选择 WebFlux。
- 尽量端到端非阻塞,避免 WebFlux + JDBC 混搭。
- 不在响应式链路中使用
.block()。 - WebClient 必须设置超时和错误处理。
- 复杂链路中使用日志和 checkpoint 辅助排查。
- 对团队不熟悉响应式时,优先使用 Spring MVC 保持可维护性。
- 网关、聚合接口、流式响应更适合 WebFlux。
- 关注 Netty 线程、连接池、背压和下游响应时间。
🔍 十一、自测问题
text
Mono 和 Flux 分别表示什么?
WebFlux 和 Spring MVC 的线程模型有什么区别?
为什么 WebFlux + JDBC 可能没有收益?
为什么不能随意 block?
WebClient 如何处理 4xx/5xx?
boundedElastic 适合什么场景?
背压解决什么问题?
什么项目不适合上 WebFlux?🧭 十二、WebFlux 选型清单
选择 WebFlux 前,先确认:
text
主要瓶颈是否是 IO 等待?
数据库访问是否有响应式驱动?
团队是否熟悉响应式调试?
是否需要流式响应或长连接?
是否能避免在链路中 block?
监控是否能覆盖 Netty 和连接池?
错误处理和超时是否完整?如果答案大多是否定,Spring MVC 通常更合适。
🧪 十三、实战案例:聚合多个远程接口
java
public Mono<UserDashboard> dashboard(Long userId) {
Mono<UserResponse> user = userClient.getUser(userId);
Mono<List<OrderResponse>> orders = orderClient.getOrders(userId).collectList();
Mono<List<CouponResponse>> coupons = couponClient.getCoupons(userId).collectList();
return Mono.zip(user, orders, coupons)
.map(tuple -> new UserDashboard(
tuple.getT1(),
tuple.getT2(),
tuple.getT3()
))
.timeout(Duration.ofSeconds(3));
}这种 IO 聚合场景适合 WebFlux,因为多个远程调用可以非阻塞组合。
如果内部每一步都是阻塞 JDBC,收益会明显降低。
📌 十四、学习建议
练习顺序:
text
先理解 Mono/Flux 的 map、flatMap、zip。
再写 WebClient 调用。
再处理 timeout 和 onErrorResume。
最后尝试流式响应 Server-Sent Events。不要一开始就把整个项目改成响应式。响应式改造应该从边界清晰的 IO 聚合场景开始。
📚 十五、常用操作符速查
| 操作符 | 作用 |
|---|---|
map | 同步转换元素 |
flatMap | 异步转换并展开 |
filter | 过滤元素 |
zip | 合并多个 Mono/Flux |
switchIfEmpty | 空结果替代 |
onErrorResume | 异常恢复 |
timeout | 超时控制 |
doOnNext | 调试或旁路观察 |
doOnError | 错误日志 |
map 和 flatMap 是最容易混淆的:
text
返回普通对象用 map。
返回 Mono/Flux 用 flatMap。📌 十六、调试建议
响应式链路调试比同步代码难。可以使用:
java
.doOnNext(value -> log.info("value={}", value))
.doOnError(error -> log.error("failed", error))
.checkpoint("load user profile")不要在调试时为了看结果到处 .block(),这会改变程序行为。
🧪 十七、流式响应场景
WebFlux 适合 Server-Sent Events:
java
@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> events() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> ServerSentEvent.builder("tick-" + i).build());
}这类长连接场景是 WebFlux 的优势之一。
📚 十八、WebFlux 与 MVC 选择表
| 场景 | 建议 |
|---|---|
| 普通后台 CRUD | Spring MVC |
| JDBC 为主 | Spring MVC |
| 聚合多个 HTTP 服务 | WebFlux 可考虑 |
| SSE/流式响应 | WebFlux |
| 团队不熟悉响应式 | Spring MVC |
| 端到端响应式驱动 | WebFlux |
选型标准不是“哪个更新”,而是团队能否稳定开发、调试和运维。
📌 十九、生产观察指标
text
Netty 事件循环线程是否被阻塞。
WebClient 连接池是否耗尽。
下游响应时间是否升高。
响应式链路错误率是否增加。
是否出现大量 timeout。WebFlux 问题通常要结合应用指标和下游指标一起看。
只看接口平均响应时间不够,还要看 P95/P99 和超时比例。
🎓 小结
WebFlux 是为响应式非阻塞场景设计的框架。它能在 IO 密集、高并发、流式响应中发挥优势,但需要端到端非阻塞和团队对响应式模型的理解。不要为了“技术更先进”而替换 Spring MVC。