Skip to content

第63课:Spring WebFlux

🎯 学习目标

  • 理解 WebFlux 的响应式、异步非阻塞模型,以及它和 Spring MVC 的区别。
  • 掌握 MonoFlux、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&lt;T&gt; 表示 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 会订阅返回的 MonoFlux,并把结果写入响应。


📖 四、函数式路由

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. 错误处理缺失

响应式链中异常不会像同步代码那样直观,需要使用 onErrorResumeonStatusdoOnError 等处理。

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错误日志

mapflatMap 是最容易混淆的:

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 选择表

场景建议
普通后台 CRUDSpring MVC
JDBC 为主Spring MVC
聚合多个 HTTP 服务WebFlux 可考虑
SSE/流式响应WebFlux
团队不熟悉响应式Spring MVC
端到端响应式驱动WebFlux

选型标准不是“哪个更新”,而是团队能否稳定开发、调试和运维。


📌 十九、生产观察指标

text
Netty 事件循环线程是否被阻塞。
WebClient 连接池是否耗尽。
下游响应时间是否升高。
响应式链路错误率是否增加。
是否出现大量 timeout。

WebFlux 问题通常要结合应用指标和下游指标一起看。

只看接口平均响应时间不够,还要看 P95/P99 和超时比例。


🎓 小结

WebFlux 是为响应式非阻塞场景设计的框架。它能在 IO 密集、高并发、流式响应中发挥优势,但需要端到端非阻塞和团队对响应式模型的理解。不要为了“技术更先进”而替换 Spring MVC。