25.1 响应式编程的背景与核心理念
在传统Servlet模型中,每个请求都会分配一个线程来处理。如果业务中存在大量的阻塞I/O操作(数据库查询、远程调用、消息队列交互等),线程会被挂起等待,直到操作完成。当并发请求数上升时,线程池会被迅速耗尽,系统吞吐量急剧下降。虽然可以通过增大线程池、引入异步Servlet等方式缓解,但底层仍是“线程-per-request”的模式,资源利用率并不高。
响应式编程(Reactive Programming) 从根本上改变了这一模型。它的核心思想是:以非阻塞的方式处理数据流,用少量线程处理大量并发请求,通过异步事件驱动机制实现高吞吐和更好的弹性。在Java生态中,响应式流规范(Reactive Streams)定义了背压(backpressure)等关键机制,而Spring WebFlux正是基于这一规范构建的非阻塞Web框架。
WebFlux并不是要淘汰Spring MVC,而是提供一个补充选项。对于CPU密集型、纯粹依赖同步逻辑的应用,Spring MVC依然合适;但对于需要处理大量并发连接且多数时间在等待外部服务的场景(网关、消息推送、实时数据流等),WebFlux的优势会非常突出。
25.2 Project Reactor:响应式编程的基石
Spring WebFlux默认使用 Project Reactor 作为响应式库,它提供了 Mono 和 Flux 两个核心类型。
- Mono<T>:表示 0 或 1 个元素的异步序列,可以类比为“一个未来会返回的T或者空结果”。常用于HTTP请求返回单个对象。
- Flux<T>:表示 0 到 N 个元素的异步序列,类似一个潜在的无限流。常用于返回列表、实时事件流等。
Reactor内置了丰富的操作符(map、flatMap、filter、zip、merge等),可以像函数式编程一样组合出复杂的异步处理管道,同时自动处理线程调度与背压。
下面是一个简单的Reactor示例,模拟从数据库查询用户然后转换为DTO的过程:
public Mono<UserDTO> getUserById(Long id) {
return userRepository.findById(id) // 返回 Mono<User>
.map(user -> new UserDTO(user.getId(), user.getName())) // 同步转换
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)));
}
当 findById 返回一个耗时操作时(例如远程调用),线程不会阻塞等待结果,而是立即释放去处理其他任务,等到数据就绪后由Reactor的调度器触发后续操作。这样,少数几个线程就能支撑起数千个并发请求。
25.3 WebFlux 的两种编程模型
Spring WebFlux提供了两种开发风格:注解式模型和函数式模型。两者都运行在相同的非阻塞容器上(Netty、Undertow、Jetty、Tomcat等),共享同样的响应式基础设施。实际项目可以根据团队习惯混合使用。
25.3.1 注解式模型(Annotated Controllers)
如果你已经熟悉Spring MVC的控制器写法,使用WebFlux的注解式模型几乎零成本切换。只需将返回值改为 Mono 或 Flux 类型即可。
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public Mono<UserDTO> getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<UserDTO> createUser(@Valid @RequestBody Mono<UserCreateRequest> requestMono) {
return requestMono.flatMap(userService::createUser);
}
@GetMapping
public Flux<UserDTO> listUsers() {
return userService.findAllUsers();
}
}
看上去和MVC几乎一样,区别在于返回值是响应式类型,并且方法参数可以接收 Mono 或 Flux(如 @RequestBody Mono<UserCreateRequest>),Spring会自动处理复杂的响应式序列化。
校验也可以无缝工作,@Valid 配合 Mono 参数依然生效,校验失败会抛出 WebExchangeBindException。
25.3.2 函数式模型(Functional Endpoints)
函数式模型更适合需要完全控制路由、请求处理流程的场景,也便于以更函数式的风格构造轻量级服务。它不需要 @Controller 等注解,而是通过 RouterFunction 和 HandlerFunction 定义路由和处理逻辑。
定义Handler
@Component
public class UserHandler {
private final UserService userService;
public UserHandler(UserService userService) {
this.userService = userService;
}
public Mono<ServerResponse> getUser(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return userService.getUserById(id)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> createUser(ServerRequest request) {
return request.bodyToMono(UserCreateRequest.class)
.flatMap(userService::createUser)
.flatMap(user -> ServerResponse.created(URI.create("/users/" + user.getId()))
.bodyValue(user));
}
public Mono<ServerResponse> listUsers(ServerRequest request) {
Flux<UserDTO> users = userService.findAllUsers();
return ServerResponse.ok().body(users, UserDTO.class);
}
}
定义Router
@Configuration
public class RouterConfig {
@Bean
public RouterFunction<ServerResponse> userRouter(UserHandler handler) {
return RouterFunctions
.route(RequestPredicates.GET("/users/{id}"), handler::getUser)
.andRoute(RequestPredicates.POST("/users"), handler::createUser)
.andRoute(RequestPredicates.GET("/users"), handler::listUsers);
}
}
函数式模型中,ServerRequest 封装了请求信息,ServerResponse 用于构建响应。这种方式不再依赖任何注解,所有逻辑显式、可追踪。Router可以组合、嵌套、添加过滤器,非常适合微服务中的轻量边界服务。
25.4 过滤器与跨切面逻辑
WebFlux提供了两种过滤器机制,分别针对注解式模型和函数式模型。
1. WebFilter(全局过滤器)
实现 WebFilter 接口,可以拦截所有进入的请求,适用于日志、安全校验、CORS处理等:
@Component
public class LoggingFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
System.out.println("Request: " + exchange.getRequest().getPath());
return chain.filter(exchange)
.doOnSuccess(aVoid -> System.out.println("Response sent"));
}
}
2. RouterFunction 过滤
在函数式模型的路由中,可以通过 RouterFunctions.route() 的 filter 方法添加过滤器,或者使用 before/after 模式插入处理逻辑:
RouterFunctions.route()
.GET("/users", handler::listUsers)
.before(request -> {
// 前置逻辑,如鉴权
return request;
})
.after((request, response) -> {
// 后置逻辑,如添加公共头
return response;
})
对于认证授权,Spring Security的响应式模块同样提供了 @EnableWebFluxSecurity 和 SecurityWebFilterChain 的支持,用法与普通Security类似,只是返回类型使用了 Mono 类型。
25.5 异常处理
在注解式模型中,可以使用与MVC相同的 @ControllerAdvice + @ExceptionHandler:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Mono<ErrorResponse> handleNotFound(UserNotFoundException ex) {
return Mono.just(new ErrorResponse("USER_NOT_FOUND", ex.getMessage()));
}
}
在函数式模型中,则需要在Handler内部显式捕获异常并构建 ServerResponse,或者利用 onErrorResume 等Reactor操作符进行降级处理。
25.6 应用场景与选型建议
WebFlux并非银弹,它的优势在I/O密集型、流处理场景中体现得最为明显:
- API网关:需要路由转发、聚合多个后端服务响应,并发量大且多数时间在等待。
- 实时消息推送、SSE(Server-Sent Events):需要维持大量长连接,服务器需主动推送数据流。
- 高并发的数据服务:如电商商品页聚合,需要同时调用库存、价格、评价等服务并合并结果。
- 文件上传/下载代理:流式处理文件内容,避免将整个文件加载到内存。
如果你的应用主要进行CPU密集计算,或者团队对响应式模型(链式调用、调度、调试)不熟悉,强上WebFlux可能带来额外的开发复杂度。Spring MVC依旧稳定可靠,两者甚至可以共存于同一个Spring Boot应用中(通过不同端口配置),按模块逐步迁移。
25.7 实践要点与注意事项
- 全链路非阻塞:WebFlux的效率建立在全栈非阻塞的基础上,如果
UserService内部使用了block()方法或者调用了阻塞的JDBC驱动,响应式带来的优势会大打折扣。应配合响应式数据库驱动(如 R2DBC、MongoDB Reactive、Redis Reactive)使用。 - 背压控制:处理流数据时需要注意消费者处理速度,避免内存溢出。Reactor内置了各种背压策略(buffer、drop、latest等),应根据场景合理选择。
- 调试的挑战:响应式编程的调用栈比命令式复杂,堆栈信息较长且难以定位。可以使用
Hooks.onOperatorDebug()开启调试模式(性能损耗较大,仅开发环境),或使用 Reactor 提供的checkpoint()操作符添加标记点。 - Servlet容器的选择:虽然WebFlux可以运行在Tomcat等传统容器上,但性能最优的搭配是Netty,它天生异步非阻塞。在Spring Boot中引入
spring-boot-starter-webflux时,默认就会使用Netty服务器。 - 与MVC的区别总结:
- MVC基于Servlet API,每个请求绑定一个线程;WebFlux基于Reactive Streams,线程不阻塞等待。
- MVC通常配合关系型数据库的JDBC(阻塞),WebFlux需要配合非阻塞数据访问层。
- MVC使用
@Controller;WebFlux支持注解式和函数式两种路由定义。 - 两者可以共存于同一个Boot应用中,但WebFlux的自动配置会取代部分MVC组件。
通过本章的学习,你应该已经掌握了WebFlux的核心概念和基础用法。在构建高吞吐、低延迟的现代Web应用时,响应式编程提供了一种强大的范式,而Spring WebFlux正是通向这一范式的成熟路径。