WebFlux:函数式路由 (Functional Routing) vs @RestController 注解式

WebFlux 两套编程模型底层都是基于 Reactor‑Netty 非阻塞,性能上限理论几乎一样,不存在某一个天生更快;差异在编程范式、代码组织、灵活性、适用场景。

底层共同点:

  • 都运行在 Reactor Netty 非阻塞容器
  • 都返回 Mono<T> / Flux<T>
  • 都不能写阻塞代码(JDBC、RestTemplate、sleep)
  • 都支持 SSE /ndjson/ 普通 json

1. 代码直观对比

① @RestController(注解式,命令式 / 注解驱动)

@RestController
@RequestMapping("/api/user")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public Flux<User> list() {
        return userService.findAll();
    }

    @GetMapping("/{id}")
    public Mono<User> getById(@PathVariable Long id) {
        return userService.findById(id);
    }
}

② Functional Routing 函数式路由(RouterFunction + HandlerFunction)

分为两部分:HandlerFunction(业务处理) + RouterFunction(路由定义)

// Handler:业务逻辑,等价于Controller的方法
@Component
public class UserHandler {
    private final UserService userService;
    public UserHandler(UserService userService) { this.userService = userService; }

    public Mono<ServerResponse> list(ServerRequest request) {
        Flux<User> flux = userService.findAll();
        return ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(flux, User.class);
    }

    public Mono<ServerResponse> getById(ServerRequest request) {
        Long id = Long.parseLong(request.pathVariable("id"));
        return userService.findById(id)
                .flatMap(user -> ServerResponse.ok().bodyValue(user))
                .switchIfEmpty(ServerResponse.notFound().build());
    }
}

// RouterFunction 路由注册,替代 @RequestMapping/@GetMapping
@Configuration
public class RouterConfig {
    @Bean
    public RouterFunction<ServerResponse> userRoutes(UserHandler userHandler) {
        return RouterFunctions.route()
                .GET("/api/user", userHandler::list)
                .GET("/api/user/{id}", userHandler::getById)
                .build();
    }
}

关键点:

  • HandlerFunction 签名:Mono<ServerResponse> handle(ServerRequest request)
  • ServerRequest:封装请求(path、param、header、body)
  • ServerResponse:构建响应(status、header、body)

2. 核心区别对比表

对比项@RestController 注解式Functional Routing 函数式路由
编程范式注解驱动,类 + 方法注解,类似 SpringMVC函数式,RouterFunction + HandlerFunction,lambda / 方法引用
请求入参注解绑定:@PathVariable @RequestParam @RequestBody手动从ServerRequest提取,无注解绑定
响应输出直接返回 Mono<T> / Flux<T>,框架自动包装手动构建ServerResponse.ok()…body(),自由度高
路由注册注解扫描自动发现手动构造 RouterFunction Bean,显式定义路由表
AOP / 拦截标准 Spring AOP、@ExceptionHandler@ControllerAdvice优先用 filter() / before() / after();@ControllerAdvice对函数路由不生效
全局异常处理@ControllerAdvice + @ExceptionHandler需要自定义 WebExceptionHandler
请求 body 解析@RequestBody Mono<User> 自动解析request.bodyToMono(User.class)手动调用
可读性对 MVC 开发者非常友好,上手快代码偏底层,新手上手成本高
动态路由很难运行时动态增删接口原生支持运行时动态组装 RouterFunction,动态增删路由
性能理论Reactor‑Netty,非阻塞Reactor‑Netty,非阻塞,理论性能一致

⚠️重点坑: @ControllerAdvice 不会作用于 Functional Routing! 函数式路由全局异常必须实现 WebExceptionHandler,不能用注解的全局异常。

3. 性能:哪个效率更高?

绝大多数业务场景,两者性能几乎无差别,不要迷信函数式更快。

  1. 底层都是 Reactor‑Netty + Reactor 流处理;
  2. @RestController 内部也是把注解元数据翻译成内部路由元信息;函数式是手动构建路由元信息;
  3. 微小差异:
    • Functional Routing:少了注解解析、参数解析的反射绑定逻辑;理论上微小的 CPU 优势,微乎其微,业务层消耗远大于这点开销
    • @RestController:参数绑定用反射 + 注解,有极少量开销,但业务接口几乎感知不到。

✅ 性能瓶颈永远是:数据库 IO、外部 HTTP 调用,不是这两套模型的选择。

只有网关类场景,大量短请求、超高 QPS,函数式的微小优势才会体现;普通业务接口,测不出差距。

4. 优缺点详细分析

✅ @RestController(注解式 WebFlux)

优点

  1. 学习成本低,从 SpringMVC 迁移过来几乎无缝;注解@Get/PostMapping@PathVariable习惯完全继承。
  2. 参数绑定自动化,不用手动写request.pathVariable()request.bodyToMono()
  3. AOP、@ControllerAdvice全局异常、校验@Valid@RequestBody校验全部开箱即用。
  4. 代码简洁,业务代码量少,日常 CRUD 业务开发效率高。

缺点

  1. 路由是注解扫描自动生成,运行时动态修改路由很麻烦,不适合动态接口场景。
  2. 请求、响应被框架封装,底层请求响应对象拿出来不够直接。
  3. 注解魔法,路由分散在各个 Controller 类,全局看所有接口需要扫描全部源码。

适合场景:普通业务后端,CRUD,团队大部分熟悉 SpringMVC;绝大多数业务 WebFlux 项目推荐这个。


✅ Functional Routing 函数式路由

优点

  1. 路由集中管理:全部接口在 RouterFunction Bean 一处集中定义,一目了然。
  2. 运行时动态路由:可以运行时拼接、修改 RouterFunction,适合网关、动态 API 场景。
  3. 请求响应完全可控:ServerRequest / ServerResponse,可以精细控制 header、status、body;没有注解黑盒。
  4. 可以链式加 filter,针对某一组路由做拦截器,粒度可以到单个路由。
// 给一组路由单独加filter
route().path("/api/internal", b -> b
        .GET("/info", handler::info)
).filter((req, next) -> {
    // 这组接口专属拦截逻辑
    return next.handle(req);
})

缺点

  1. 上手门槛高,要熟悉ServerRequest/ServerResponse;每个参数、body 都要手动解析。
  2. @ControllerAdvice、@Valid 全局校验不生效,异常处理、参数校验需要自己封装。
  3. 样板代码变多:每次都要写ServerResponse.ok().body(...),写 CRUD 会啰嗦。
  4. AOP 切面很难切入 HandlerFunction,不能直接用@Around切 handler 方法。

适合场景

  1. 自建网关、内部代理服务;
  2. 需要运行时动态增删路由
  3. 需要细粒度给不同路由分组加 filter;
  4. 追求极致底层控制,接口数量不多。

5. 选型建议

  1. 普通业务系统(CRUD、后台服务)👉优先 @RestController

开发效率高,团队维护成本低,异常、校验、AOP 全部原生支持,性能足够。

  1. 做网关、反向代理、动态接口平台👉优先 Functional Routing

集中路由、动态组装、细粒度 filter 是它的强项。

  1. 可以混合使用!同一个项目可以同时存在 @RestController 和 RouterFunction,互不冲突。

6. 常见踩坑

  1. 函数路由不要指望@ControllerAdvice,必须实现WebExceptionHandler做全局异常;
  2. @Valid校验在 HandlerFunction 不会自动触发,需要手动调用校验器;
  3. 不要误以为函数式就一定更快,业务系统几乎感知不到性能差异;
  4. 无论哪种模型,都禁止写阻塞代码(Thread.sleep、同步 JDBC),否则非阻塞优势全部丧失。

补充小示例:函数路由全局 filter

@Bean
public RouterFunction<ServerResponse> routes(UserHandler handler) {
    return RouterFunctions.route()
            .GET("/api/user", handler::list)
            .GET("/api/user/{id}", handler::getById)
            .filter((request, next) -> {
                // 全局日志、鉴权
                System.out.println("path:" + request.path());
                return next.handle(request);
            })
            .build();
}

##########################

附:RouterFunction<ServerResponse> 与 RouterFunctionMapping

结论:

  1. 如果你把 RouterFunction<ServerResponse> 注册为 Spring Bean,RouterFunctionMapping 会自动发现、收集、合并所有 RouterFunction Bean,自动注册为路由映射,不需要手动注册 RouterFunctionMapping
  2. Spring WebFlux 自动配置 WebFluxAutoConfiguration 内部会创建 RouterFunctionMapping,它会从容器查找全部 RouterFunction<?> bean,做组合 RouterFunctions.and()
  3. 也可以手动构建 RouterFunctionMapping Bean(高级场景,覆盖自动行为)。

注意:这是 WebFlux 函数式编程模式,和 @Controller/@RequestMapping 两套体系并存,可以混合使用。

1. 最简示例:把 RouterFunction 声明为 @Bean(自动被 RouterFunctionMapping 拾取)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;

@Configuration
public class MyRouteConfig {

    // ✅ 直接返回 RouterFunction<ServerResponse> Bean,自动被 RouterFunctionMapping 收集
    @Bean
    public RouterFunction<ServerResponse> helloRoute() {
        return route(GET("/hello"),
                request -> ServerResponse.ok().bodyValue("hello webflux router function"));
    }

    // 多个 RouterFunction Bean,会被自动合并
    @Bean
    public RouterFunction<ServerResponse> userRoute() {
        return route(GET("/user/{id}"), request -> {
            String id = request.pathVariable("id");
            return ServerResponse.ok().bodyValue("userId:" + id);
        });
    }
}
  • 启动后访问 /hello/user/123 直接生效。
  • RouterFunctionMapping 内部会拿到上下文所有 RouterFunction,执行 router1.and(router2).and(router3)...

2. 手动构造 RouterFunctionMapping Bean(接管,不使用自动收集)

如果你不想 Spring 自动收集所有 RouterFunction Bean,可以自己定义 RouterFunctionMapping,传入你手动组装好的 RouterFunction。

@Bean
public RouterFunctionMapping routerFunctionMapping(RouterFunction<ServerResponse> combinedRouter) {
    RouterFunctionMapping mapping = new RouterFunctionMapping(combinedRouter);
    // 可选:设置顺序,和 @Controller 映射做优先级控制
    mapping.setOrder(-1);
    return mapping;
}

// 手动合并,只有这里的路由生效,容器里其他 RouterFunction Bean 会被忽略!
@Bean
public RouterFunction<ServerResponse> combinedRouter() {
    return route(GET("/manual"), req -> ServerResponse.ok().bodyValue("manual mapping"));
}

⚠️关键点:一旦你自己实例化 new RouterFunctionMapping(myRouter)它不会再自动去 Spring 容器捞其他 RouterFunction Bean,只使用你传入的那一个。

3. RouterFunctions.route () 常用完整示例(POST + json body)

@Bean
public RouterFunction<ServerResponse> orderRouter() {
    return route()
            .GET("/order/{orderId}", request -> ServerResponse.ok().bodyValue("order:" + request.pathVariable("orderId")))
            .POST("/order", request ->
                    request.bodyToMono(Order.class)
                            .flatMap(order -> ServerResponse.ok().bodyValue("receive:" + order.getName()))
            )
            .build();
}

static class Order {
    private String name;
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

4. 关键底层细节(RouterFunctionMapping)

RouterFunctionMapping 源码逻辑简述:

// Spring WebFlux 自动配置逻辑伪代码
Collection<RouterFunction<?>> routerFunctions = context.getBeansOfType(RouterFunction.class).values();
RouterFunction<?> composite = routerFunctions.stream()
        .reduce(RouterFunction::and)
        .orElse(null);
return new RouterFunctionMapping(composite);
  • 容器有多个 RouterFunction bean → 自动 .and() 合并为一个复合路由。
  • 没有任何 RouterFunction Bean:RouterFunctionMapping 依然存在,只是无路由规则。

5. 容易踩坑点

  1. ❌ 不要只写 RouterFunction<ServerResponse> fn = route(...); 不交给 Spring 管理(局部变量,不是 Bean,RouterFunctionMapping 看不到,不会生效)。
  2. 函数式路由 RouterFunction 和注解式 @Controller 可以共存,DispatcherHandler 会分别调用 RequestMappingHandlerMappingRouterFunctionMapping
  3. 自己 new RouterFunctionMapping 传入自定义路由时,不再自动收集容器其他 RouterFunction
  4. 过滤器:.filter() 在 RouterFunction 层面做;全局过滤器用 WebFilter

6. 增加 filter 示例

@Bean
public RouterFunction<ServerResponse> filteredRoute() {
    return route(GET("/demo"), req -> ServerResponse.ok().bodyValue("demo"))
            .filter((request, next) -> {
                System.out.println("request path:" + request.path());
                return next.handle(request);
            });
}

总结记忆

  • @Bean RouterFunction<ServerResponse>自动被 RouterFunctionMapping 收集合并,开箱即用
  • ✅ 多个 RouterFunction Bean,自动 and () 合并。
  • ⚠️ 手动 new RouterFunctionMapping(xxx),只使用你传入的路由,不再自动扫描容器其他 RouterFunction。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值