本节目标:讲清 WebFlux 与 MVC 是两条独立栈(不是替换关系)、
DispatcherHandler取代DispatcherServlet后的处理链、返回Mono的控制器方法如何被适配成响应,以及WebFilter与函数式端点的定位。
适用版本:Spring Boot 4.1.x(Java 21)
5.2 WebFlux 请求处理
4.1 拆的是 MVC 那条链:DispatcherServlet → HandlerMapping → HandlerAdapter → HandlerResult。WebFlux 长得像,但每一个环节的类型都换成了响应式版本,而且它不是 MVC 的升级版——两条栈并存,各自解决不同问题。本节用本机 spring-webflux-7.0.9.jar 与 spring-web-7.0.9.jar 把这条链的每一环核实一遍。
延续借阅场景:本节用 GET /borrowings/{memberId} 返回该读者借阅记录流,GET /borrowings/{memberId}/events 用 SSE 推送借阅事件。
5.2.1 两条独立的栈,不是替换关系
先纠正一个常见误解:WebFlux 不是「MVC 的下一代」。它们是两套平行实现:
| 维度 | Spring MVC | Spring WebFlux |
|---|---|---|
| 起步依赖(4.x 新名) | spring-boot-starter-webmvc | spring-boot-starter-webflux |
| 运行模型 | 阻塞式,一请求一线程 | 非阻塞,事件循环 + 少量线程 |
| 核心入口 | DispatcherServlet | DispatcherHandler |
| 底层契约 | HttpServletRequest / HttpServletResponse | ServerWebExchange |
| 默认服务器 | Tomcat(Servlet 容器) | Reactor Netty |
| 阻塞代码 | 天然支持 | 必须隔离,否则拖垮事件循环 |
两者不能同时启用。Boot 通过 org.springframework.boot.webflux.WebFluxWebApplicationTypeDeducer(本机 spring-boot-webflux-4.1.1.jar 核实)判断应用类型:类路径上只有 DispatcherHandler 就推断为 reactive,只有 DispatcherServlet 就推断为 servlet;两个都在(比如误引了两个 starter)会启动失败并提示需要显式指定 spring.main.web-application-type。
选型不看「谁更先进」,而看「瓶颈在哪」:大量外部 I/O、流式响应、需要长连接时 WebFlux 才有意义;以数据库同步访问为主的服务,用 MVC + 虚拟线程往往比硬改 WebFlux 更划算(虚拟线程见 7.2)。
5.2.2 DispatcherHandler:取代 DispatcherServlet 的入口
WebFlux 的入口是 org.springframework.web.reactive.DispatcherHandler(本机 spring-webflux-7.0.9.jar 核实):
public class DispatcherHandler
implements org.springframework.web.server.WebHandler,
org.springframework.web.cors.reactive.PreFlightRequestHandler,
org.springframework.context.ApplicationContextAware {
public reactor.core.publisher.Mono<Void> handle(org.springframework.web.server.ServerWebExchange);
protected void initStrategies(org.springframework.context.ApplicationContext);
public java.util.List<org.springframework.web.reactive.HandlerMapping> getHandlerMappings();
}
三点与 MVC 直接对应:
- 入口方法是
handle(ServerWebExchange): Mono<Void>,而不是service(request, response): void。它返回的Mono<Void>代表「整个请求处理完成」这一个信号——请求的完成本身也是一个响应式信号。 - 策略在
initStrategies(ApplicationContext)里从容器里捞(而不是 MVC 的initStrategies从DispatcherServlet.properties兜底),所以 WebFlux 的策略 bean 是容器管理的普通 bean,可以直接注入替换。 handle内部是声明式的链:找 handler → 找 adapter → 执行 → 处理结果 → 渲染异常,每一环都是flatMap串起来的Mono,而不是嵌套调用。
DispatcherHandler 自己也实现了 WebHandler,这正是它接入过滤器链的方式(见 5.2.5)。
5.2.3 HandlerMapping 与 HandlerAdapter 的响应式版本
两个接口的签名都换了类型:
public interface org.springframework.web.reactive.HandlerMapping {
reactor.core.publisher.Mono<Object> getHandler(ServerWebExchange exchange);
}
public interface org.springframework.web.reactive.HandlerAdapter {
boolean supports(Object handler);
reactor.core.publisher.Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler);
}
差别不只是「返回值包了 Mono」:
HandlerMapping.getHandler返回Mono<Object>:因为找 handler 的过程可能本身是异步的(比如要做响应式的内容协商)。空Mono表示没找到,DispatcherHandler据此返回 404。HandlerAdapter.handle返回Mono<HandlerResult>:注意是HandlerResult(本机核实为org.springframework.web.reactive.HandlerResult),里面装着handler、returnValue、returnType、BindingContext——方法调用已经完成,但返回值还没被渲染。这与 MVC 里「HandlerAdapter返回ModelAndView」是同一个思路。
本机核实的三类 adapter 与它们的适用对象:
| HandlerAdapter | 支持的 handler | 用途 |
|---|---|---|
RequestMappingHandlerAdapter | HandlerMethod | 注解式 @RequestMapping 方法 |
HandlerFunctionAdapter | HandlerFunction | 函数式端点(RouterFunction 路由到的) |
SimpleHandlerAdapter | 实现 WebHandler 的对象 | 兜底:supports 判断对象是否 WebHandler,是则直接 handle |
对应地,WebFlux 也有两类 HandlerMapping:RequestMappingHandlerMapping(注解式,位于 org.springframework.web.reactive.result.method.annotation)与 RouterFunctionMapping(函数式,位于 org.springframework.web.reactive.function.server.support)。它们都由 WebFluxConfigurationSupport 注册成 bean——本机 javap 能看到 webHandler()、requestMappingHandlerMapping(...)、routerFunctionMapping(...)、resourceHandlerMapping(...) 等工厂方法。
5.2.4 返回 Mono 的控制器方法如何被适配
控制器写起来和 MVC 很像:
@RestController
public class BorrowingController {
private final BorrowingService borrowingService;
public BorrowingController(BorrowingService borrowingService) {
this.borrowingService = borrowingService;
}
@GetMapping("/borrowings/{memberId}")
public Flux<Borrowing> list(@PathVariable String memberId) {
return borrowingService.findByMember(memberId);
}
@GetMapping(value = "/borrowings/{memberId}/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<BorrowingEvent> events(@PathVariable String memberId) {
return borrowingService.events(memberId);
}
}
但内部适配路径完全不同。RequestMappingHandlerAdapter 调用方法拿到返回值后,不会像 MVC 那样交给 HandlerMethodReturnValueHandler 去 writeWithMessageConverters,而是:
- 用
ReactiveAdapterRegistry判断返回值是不是「响应式类型」(Mono/Flux/ Kotlin 协程 / RxJava /CompletableFuture); - 把它适配成一个统一的
Publisher,保持惰性,不在这里block; - 产出
HandlerResult,交给HandlerResultHandler渲染。
本机核实的 HandlerResultHandler 实现,各自负责一类返回值:
| 结果处理器 | 负责的返回值 |
|---|---|
ResponseBodyResultHandler | @ResponseBody / @RestController 的响应体(含 Flux → SSE / 流式 JSON) |
ResponseEntityResultHandler | 返回 ResponseEntity 的方法 |
ViewResolutionResultHandler | 返回视图名 / ModelAndView(含 SSE 的 SseEmitter 等价物) |
ServerResponseResultHandler | 函数式端点的 ServerResponse |
WebFluxResponseStatusExceptionHandler | WebExceptionHandler 分支,把异常映射成响应状态 |
关键设计意图:整个链上没有一次阻塞。MVC 里 HandlerAdapter 必须把响应体当场写进 HttpServletResponse;WebFlux 里 HandlerResultHandler 返回的仍是一个 Mono<Void>,真正的写由 ServerHttpResponse 在订阅时驱动。这就是「返回 Mono 的接口能支撑高并发」的原因——线程在等待 I/O 期间被释放回事件循环。
5.2.5 WebFilter 与 Servlet Filter 的差异
WebFilter 定义在 spring-web 的 org.springframework.web.server 包(本机核实,不在 spring-webflux):
public interface org.springframework.web.server.WebFilter {
reactor.core.publisher.Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}
public interface org.springframework.web.server.WebFilterChain {
reactor.core.publisher.Mono<Void> filter(ServerWebExchange exchange);
}
与 Servlet Filter 的差别是一组连锁反应:
| 维度 | Servlet Filter | WebFilter |
|---|---|---|
| 签名 | doFilter(req, resp, chain) 返回 void | filter(exchange, chain) 返回 Mono<Void> |
| 参数 | 两个对象(请求 / 响应) | 一个 ServerWebExchange(请求 + 响应 + 属性) |
| 「放行」 | chain.doFilter(req, resp) | chain.filter(exchange) |
| 「响应之后」 | 写在 chain.doFilter 调用之后的代码 | 用 then(Mono) 挂到返回的 Mono 上 |
| 是否阻塞 | 是,一请求占一线程 | 否,返回的信号驱动 |
第四行是最容易写错的地方。Servlet 里「记录耗时」写在 chain.doFilter 之后即可;WebFlux 里 chain.filter(exchange) 是立即返回的(它只是返回一个 Mono),后面的代码会在请求处理之前就执行。正确写法是把它接到返回信号上:
@Component
public class TimingWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
long start = System.nanoTime();
return chain.filter(exchange)
.doFinally(signal -> {
long cost = (System.nanoTime() - start) / 1_000_000;
System.out.println(exchange.getRequest().getPath() + " -> " + cost + " ms");
});
}
}
过滤器链的组装在 org.springframework.web.server.adapter.WebHttpHandlerBuilder(本机核实)里:它把 WebFilter 列表、WebExceptionHandler 列表与 DispatcherHandler 包成一个 HttpHandler。链的具体实现是 org.springframework.web.server.handler.DefaultWebFilterChain,构造签名 (WebHandler, List<WebFilter>)——链尾固定是那个 WebHandler(也就是 DispatcherHandler),每个 filter 调 chain.filter 就前进一环。
异常处理走另一条接口:WebExceptionHandler.handle(ServerWebExchange, Throwable): Mono<Void>(本机核实)。Boot 的 WebFlux 自动配置提供了 org.springframework.boot.webflux.autoconfigure.error.AbstractErrorWebExceptionHandler 作为默认实现(本机 spring-boot-webflux-4.1.1.jar 核实),它负责把未捕获异常渲染成 /error 响应。
5.2.6 函数式端点:RouterFunction 与 HandlerFunction
注解式之外,WebFlux 提供了一套函数式 API。核心是两个函数式接口(本机核实):
public interface org.springframework.web.reactive.function.server.HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest request);
}
public interface org.springframework.web.reactive.function.server.RouterFunction<T extends ServerResponse> {
Mono<HandlerFunction<T>> route(ServerRequest request);
}
注意 RouterFunction.route 返回的是 Mono<HandlerFunction<T>>——「找到的处理器」本身也是异步找到的,找不到返回空 Mono(对应 404)。写法:
@Configuration
public class BorrowingRouter {
@Bean
public RouterFunction<ServerResponse> borrowingRoutes(BorrowingHandler handler) {
return RouterFunctions.route()
.GET("/fn/borrowings/{memberId}", handler::list)
.nest(path("/fn/borrowings/{memberId}"), builder -> builder
.GET("/events", handler::events))
.build();
}
}
@Component
public class BorrowingHandler {
private final BorrowingService borrowingService;
public BorrowingHandler(BorrowingService borrowingService) {
this.borrowingService = borrowingService;
}
public Mono<ServerResponse> list(ServerRequest request) {
String memberId = request.pathVariable("memberId");
return ServerResponse.ok().body(borrowingService.findByMember(memberId), Borrowing.class);
}
public Mono<ServerResponse> events(ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(borrowingService.events(request.pathVariable("memberId")), BorrowingEvent.class);
}
}
本机核实的静态入口:RouterFunctions.route()(返回 Builder)、RouterFunctions.route(RequestPredicate, HandlerFunction)、RouterFunctions.nest(...)、RouterFunctions.resources(...)、RouterFunctions.toWebHandler(...)。ServerResponse 提供 ok()、status(int)、created(URI)、badRequest()、unprocessableContent() 等(本机核实)。
两种风格的取舍:
| 维度 | 注解式 | 函数式 |
|---|---|---|
| 可读性 | 声明式,路由与实现同处一地 | 路由集中在一处,处理逻辑分开 |
| 组合能力 | 靠 @RequestMapping 组合注解 | 原生可组合:and / andNest / filter |
| 复用与测试 | 需要起上下文 | RouterFunction 是普通对象,可直接单测路由 |
| 生态适配 | 注解、参数解析器、验证全都现成 | 需手写 ServerRequest 解析 |
| 适合场景 | 常规 CRUD、团队熟悉 MVC | 网关式转发、动态路由、需要程序化拼装路由 |
选择建议:默认用注解式,只有在「路由需要按条件程序化拼装」「同一 handler 要挂到多组路径」「想脱离 Spring 上下文测路由」时才上函数式。两者可以并存——RequestMappingHandlerMapping 与 RouterFunctionMapping 是两个并列的 HandlerMapping,DispatcherHandler 按顺序问过去,谁先匹配谁处理。
5.2.7 自动配置做了什么
Boot 4.x 把 WebFlux 相关自动配置收进了独立模块 spring-boot-webflux(本机核实存在,对应 spring-boot-starter-webflux;starter 名在 4.0 未被改名)。关键类:
| 类 | 作用 |
|---|---|
org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration | 启用 WebFlux,注册静态资源、欢迎页、表单/会话等 |
org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration | 把 WebFilter / WebExceptionHandler / DispatcherHandler 组装成 HttpHandler |
org.springframework.boot.webflux.autoconfigure.error.AbstractErrorWebExceptionHandler | 默认错误响应渲染 |
org.springframework.boot.webflux.WebFluxWebApplicationTypeDeducer | 推断应用类型 |
常用配置项(本机 spring-boot-webflux-4.1.1.jar 的元数据核实):spring.webflux.base-path(统一前缀)、spring.webflux.static-path-pattern、spring.webflux.default-html-escape、spring.webflux.problemdetails.enabled,以及 4.0 新增的 spring.webflux.apiversion.*(API 版本化,与 MVC 的 spring.mvc.apiversion.* 对应)。
要替换默认行为,注入 WebFluxConfigurer(本机核实接口)即可——configureHttpMessageCodecs、addFormatters、addCorsMappings、configurePathMatching、configureArgumentResolvers 等都是默认方法,按需覆写。
5.2.8 本机可以做的验证
export JAVA_HOME=/tmp/springboot_book/jdk-21.0.12.1+1/Contents/Home
J=/tmp/springboot_book/jars
SW=~/.m2/repository/org/springframework/spring-web/7.0.9/spring-web-7.0.9.jar
# DispatcherHandler 的接口与入口方法
"$JAVA_HOME/bin/javap" -cp "$J/spring-webflux-7.0.9.jar" org.springframework.web.reactive.DispatcherHandler
# 响应式 HandlerMapping / HandlerAdapter 的签名
"$JAVA_HOME/bin/javap" -cp "$J/spring-webflux-7.0.9.jar" org.springframework.web.reactive.HandlerMapping
"$JAVA_HOME/bin/javap" -cp "$J/spring-webflux-7.0.9.jar" org.springframework.web.reactive.HandlerAdapter
# WebFilter 其实在 spring-web 里
"$JAVA_HOME/bin/javap" -cp "$SW" org.springframework.web.server.WebFilter
"$JAVA_HOME/bin/javap" -cp "$SW" org.springframework.web.server.handler.DefaultWebFilterChain
# 函数式端点的两个接口
"$JAVA_HOME/bin/javap" -cp "$J/spring-webflux-7.0.9.jar" \
org.springframework.web.reactive.function.server.RouterFunction
要观察整条链,最快的办法是在 DispatcherHandler.handle 与 DefaultWebFilterChain.filter 下断点,请求一次 /borrowings/m-1:断点会依次命中 filter → dispatcher → mapping → adapter → result handler。
5.2.9 知道之后能做什么
排「请求挂住」。 若某个接口一直不返回,先确认 HandlerAdapter 返回的 Mono 是否被订阅、以及链上是否混入了 block()(下一节展开)。
排「过滤器顺序不对」。 WebFilter 的顺序由 @Order / Ordered 决定,不是声明顺序。需要「认证先于日志」时,显式给 @Order。
排「404 但路由看着没错」。 记住 HandlerMapping.getHandler 返回空 Mono 就是 404 的来源;注解式与函数式是两个并列 mapping,函数式路由若被前面的 mapping 抢走,要检查路径是否重叠。
小结
- WebFlux 与 MVC 是两条独立栈,起步依赖分别是
spring-boot-starter-webflux与spring-boot-starter-webmvc,不能同时启用。 DispatcherHandler.handle返回Mono<Void>;HandlerMapping.getHandler返回Mono<Object>,HandlerAdapter.handle返回Mono<HandlerResult>,整条链无阻塞。- 返回
Mono/Flux的控制器方法由ReactiveAdapterRegistry适配后交给HandlerResultHandler渲染,真正的写发生在订阅时。 WebFilter在spring-web的org.springframework.web.server包,返回Mono<Void>;「响应之后」的逻辑要接在返回信号上,不能写在chain.filter之后。- 函数式端点(
RouterFunction/HandlerFunction)与注解式并列存在,适合程序化拼装路由,不是替代关系。
背压有了、请求链也有了,剩下最后一环:如果链上某处必须调用阻塞 API,怎么放才不拖垮事件循环。
阅读导航:上一节:5.1 响应式类型与背压 · 下一节:5.3 阻塞代码的隔离 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。