概述
前文系列已完整建立了 Spring MVC 的请求处理帝国:从 DispatcherServlet 的启动,到 @Controller 注解的映射、参数解析、消息转换与异常处理。这些机制深深依赖于 Java 注解和反射。Spring 5.2 引入的函数式 Web 端点(WebMvc.fn)则提供了一条截然不同的路径:它放弃了注解和反射,转而使用纯粹的 Lambda 表达式来定义路由和处理逻辑。本文将深入这一新范式的内部,揭示它如何无缝融入传统的 DispatcherServlet,以及它在现代微服务和轻量级端点中所带来的独特优势。
函数式 Web 端点代表了 Spring 对“代码即配置”理念的极致追求。与传统的 @RequestMapping 注解模型不同,RouterFunction 将 HTTP 谓词和处理器组合成了可读的、强类型的函数式管道。这种范式不仅避免了运行时的注解扫描,使启动速度更快、内存占用更低,而且将路由定义从静态的类结构中解放出来,变得可以动态组合、条件化注册以及轻松进行单元测试。然而,它并非注解模型的完全替代品,而是为特定场景(如轻量级 API 网关、动态路由、无注解的微服务)提供了更优的设计。本文将深入剖析 RouterFunction 如何被 DispatcherServlet 接纳,如何复用 Spring MVC 体系中的消息转换器、异常处理器和拦截器,从而在函数式世界和 Servlet 世界之间架起一座高效互通的桥梁。
核心要点
- 核心三角:
RouterFunction(路由)、HandlerFunction(处理)、RequestPredicate(匹配规则)。 - 路由解析:
RouterFunction如何被编译为内部的路由映射表,实现 O(1) 或有序匹配。 - 与 MVC 基础设施的关系:
RouterFunctionMapping作为一个HandlerMapping,使得函数式端点与注解端点共享相同的DispatcherServlet、HandlerInterceptor和HandlerExceptionResolver。 - 参数解析与校验:
ServerRequest.body()如何利用HttpMessageConverter,以及如何实现功能强大的Validator校验。 - 设计取舍:函数式的灵活性/高内聚 vs 注解的约定俗成/AOP 支持程度。
文章组织架构图
flowchart TD
n1["1. 函数式Web总览:RouterFunction的设计哲学"] --> n2["2. 核心接口:HandlerFunction、RouterFunction与RequestPredicate"]
n2 --> n3["3. 请求与响应:ServerRequest/ServerResponse与消息转换器的深度协作"]
n3 --> n4["4. 路由解析与挂载:RouterFunctionMapping如何融入DispatcherServlet"]
n4 --> n5["5. 参数提取、校验与异常处理在函数式管道中的实现"]
n5 --> n6["6. 与 @Controller 注解模型的对比与抉择"]
n6 --> n7["7. 生产事故排查专题"]
n6 --> n8["8. 面试高频专题"]
classDef default fill:#f8f9fa,stroke:#333,stroke-width:1px,color:#333;
架构图说明
- 总览说明:全文 8 个模块从函数式端点的设计哲学出发,深入接口、路由、集成、校验以及对比,最后通过事故和面试进行实践闭环。
- 逐模块说明:模块 1-2 建立核心编程模型;模块 3-4 揭示其内部如何利用 Spring MVC 已有的转换和调度机制;模块 5 关注校验与异常;模块 6 进行客观的技术选型;模块 7-8 落地问题与应试。
- 关键结论:WebMvc.fn 并非要颠覆 Spring MVC,而是通过复用 DispatcherServlet 的完整基础设施,为特定场景提供了一种更轻量、更纯粹的函数式路由选择。
1. 函数式 Web 总览:RouterFunction 的设计哲学
1.1 从注解到函数:为什么需要 WebMvc.fn
传统的 Spring MVC 依赖 @Controller、@RequestMapping 以及方法参数上的 @RequestParam、@RequestBody 等注解来定义 Web 端点。这套模型在近十年中证明了自身的强大,但存在三个固有的工程痛点:
- 启动开销:注解模型在容器启动时必须扫描所有
@Controller类,解析每个@RequestMapping方法,构建HandlerMethod对象并注册到RequestMappingHandlerMapping。在大型单体应用中,数千个HandlerMethod的创建和注册会显著拖慢启动时间,并占用大量元数据内存。 - 反射与代理依赖:注解的语义通过运行时反射实现,且方法级安全、事务、缓存等切片必须依靠 AOP 代理(JDK 动态代理或 CGLIB),这增加了运行时开销,也使得代码的执行流不够透明。
- 测试复杂度:尽管 Spring 提供了 MockMvc,单元测试一个
@Controller方法仍然需要启动 Spring 上下文(或加载部分上下文),且输入输出的模拟较为笨重。
函数式 Web 端点(WebMvc.fn)以 Lambda 表达式 取代注解,将路由规则和处理逻辑显式地组合成不可变对象。这带来了:
- 无反射、无 CGLIB 代理:框架直接将
HandlerFunction作为普通函数调用,无需解析注解或生成代理类。 - 快速启动与低内存占用:
RouterFunction由开发者显式定义为 Bean,无扫描阶段,直接注册到RouterFunctionMapping。 - 纯粹的函数式可组合性:路由可以像集合一样被过滤、分组、嵌套和逻辑运算高阶组合。
- 单元测试极致简单:测试一个
HandlerFunction只需手工构建ServerRequest,调用handle(request)并验证ServerResponse,一切皆为纯 Java 对象。
1.2 函数式端点的定位:互补而非替代
WebMvc.fn 并非设计用来替代 @Controller。它更适合下列场景:
- 轻量级 API 网关或边缘服务:路由规则相对固定,但要求极低的资源消耗和快速启动。
- 动态路由框架或定制需求:需要根据配置或外部数据动态生成路由时,编程式的函数管道比静态注解灵活得多。
- 函数式编程范式的偏好:团队主张显式组合,避免隐式的“魔法”。
对于复杂的业务 CRUD,尤其是强依赖方法级安全、声明式事务或 @Valid 自动校验的场景,@Controller 仍是主流选择。两者可以在同一个 DispatcherServlet 下共存,Spring 允许根据 HandlerMapping 的优先级和平共处。
1.3 WebMvc.fn 核心接口类图
函数式端点的核心模型由五个关键接口构成,它们完全相对于 Servlet API 的抽象,但设计上更加函数式和不可变。
classDiagram
direction LR
class RouterFunction~T extends ServerResponse~ {
<<interface>>
+route(ServerRequest) Optional~HandlerFunction~T~~
}
class HandlerFunction~T extends ServerResponse~ {
<<interface>>
+handle(ServerRequest) T
}
class RequestPredicate {
<<interface>>
+test(ServerRequest) boolean
+and(RequestPredicate) RequestPredicate
+or(RequestPredicate) RequestPredicate
+negate() RequestPredicate
}
class ServerRequest {
<<interface>>
+pathVariable(String) String
+queryParam(String) Optional~String~
+body(Class~T~) T
+body(ParameterizedTypeReference~T~) T
+headers() Headers
+method() HttpMethod
+uri() URI
}
class ServerResponse {
<<interface>>
+status(HttpStatus) Builder
+ok() Builder
+created(URI) Builder
+noContent() Builder
}
class Builder {
+header(String, String) Builder
+cookie(ResponseCookie) Builder
+body(Object) ServerResponse
+build() ServerResponse
}
RouterFunction --> HandlerFunction : returns
RouterFunction o-- RequestPredicate : uses
HandlerFunction ..> ServerRequest : accepts
HandlerFunction ..> ServerResponse : returns
ServerRequest --> HttpMessageConverter : uses for body()
ServerResponse --> HttpMessageConverter : uses for body building
ServerResponse *-- Builder : inner builder
图表主旨概括:此图展示了 WebMvc.fn 编程模型的核心接口及其依赖关系,凸显函数式三角 RouterFunction、HandlerFunction、RequestPredicate 如何围绕不可变的 ServerRequest/ServerResponse 构成闭环。
逐层/逐元素分解:
RouterFunction作为顶层路由容器,接收ServerRequest并返回可能存在的HandlerFunction,这等价于HandlerMapping的getHandler语义。RequestPredicate采用组合模式,可进行and、or、negate逻辑运算,替换@RequestMapping的多个属性匹配。HandlerFunction是纯粹的函数式接口(ServerRequest) -> ServerResponse,它取代了传统 Controller 方法。ServerRequest提供了类似HttpServletRequest的只读抽象,而ServerResponse通过建造者模式构建不可变响应。
设计原理映射:
- 策略模式:
HandlerFunction的不同实现是不同的处理策略。 - 组合模式:
RouterFunction与RequestPredicate均可通过组合形成复杂逻辑。 - 建造者模式:
ServerResponse的构建过程分离了响应对象的复杂创建。
工程联系与关键结论:整个函数式端点模型是对 Servlet API 的提升抽象,但最终仍然通过适配器与底层 Servlet 流交互,它完全活在 DispatcherServlet 的生态中。
2. 核心接口:HandlerFunction、RouterFunction 与 RequestPredicate
2.1 HandlerFunction:纯函数处理器
HandlerFunction 的定义极其简洁,是一个单抽象方法接口:
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
T handle(ServerRequest request) throws Exception;
}
它表示一个从 ServerRequest 到 ServerResponse 的函数。所有异常都可以直接抛出,由外层 HandlerFunctionAdapter 传递给 DispatcherServlet 的 HandlerExceptionResolver 链(详见第 6 篇异常处理)。与 @Controller 方法不同,HandlerFunction 没有固定的方法签名,不依赖反射调用,执行性能极高。
示例:简单文字返回
HandlerFunction<ServerResponse> helloHandler = request ->
ServerResponse.ok().body("Hello, WebMvc.fn!");
这里 Lambda 本身就充当了“控制器方法”,直接构造 ServerResponse。没有 HttpServletResponse 的写入,响应对象完全不可变。
2.2 RouterFunction:请求路由的函数组合子
RouterFunction 是函数式路由的核心接口:
@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
Optional<HandlerFunction<T>> route(ServerRequest request);
default RouterFunction<T> and(RouterFunction<T> other) { ... }
default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handler) { ... }
// 静态方法:route(RequestPredicate, HandlerFunction)
}
其语义为:给定一个请求,返回可能匹配的 HandlerFunction。这完美对应了 HandlerMapping.getHandler 的契约。多个 RouterFunction 可以通过 and 组合成一个整体,而 route 方法按添加顺序依次尝试匹配,一旦找到则为 Optional.of,否则为 Optional.empty。
2.3 RequestPredicate:声明式匹配规则
RequestPredicate 是一个 Predicate<ServerRequest> 的特殊化:
@FunctionalInterface
public interface RequestPredicate extends Predicate<ServerRequest> {
boolean test(ServerRequest request);
default RequestPredicate and(RequestPredicate other) {
return request -> test(request) && other.test(request);
}
default RequestPredicate or(RequestPredicate other) { ... }
default RequestPredicate negate() { ... }
}
Spring 提供了 RequestPredicates 工厂类,静态方法涵盖了所有常见匹配维度:
GET(String pattern)、POST(String pattern)等 —— 匹配 HTTP 方法和路径模式path(String pattern)—— 只匹配路径contentType(MediaType)—— 匹配 Content-Type 请求头accept(MediaType)—— 匹配 Accept 请求头headers(Predicate<Headers>)—— 自定义请求头匹配param(String name, String value)—— 查询参数匹配
这些谓词可以通过 and/or 自由组合,形成复杂的路由条件。这与 @RequestMapping 的多个属性(method、path、headers、params 等)等价,但表达力更强,因为可以使用任意逻辑组合。
2.4 RouterFunctions 静态工厂与 DSL 风格
实际开发中不会直接 new RouterFunction,而是使用 RouterFunctions 静态辅助方法:
@Bean
RouterFunction<ServerResponse> userRoutes() {
return RouterFunctions.route(
RequestPredicates.GET("/users/{id}"),
request -> ServerResponse.ok().body("User " + request.pathVariable("id"))
)
.andRoute(
RequestPredicates.POST("/users").and(RequestPredicates.contentType(MediaType.APPLICATION_JSON)),
request -> {
User user = request.body(User.class);
// ... 保存用户逻辑
return ServerResponse.created(URI.create("/users/" + user.getId())).build();
}
);
}
RouterFunctions.route(predicate, handler) 创建了一个仅包含单一路由的 RouterFunction,其 route 方法会检查谓词,通过则返回该 handler。随后用 andRoute 追加新的路由。这种链式 DSL 可以非常清晰地将相关路由组织在一个方法里。
2.5 路由的组合:add、andRoute、nest 与分组
除了 andRoute,还有 nest 方法用来创建路由前缀(等价于 @RequestMapping 在类级定义前缀路径):
@Bean
RouterFunction<ServerResponse> apiRoutes() {
return RouterFunctions.nest(
RequestPredicates.path("/api"),
RouterFunctions.route(
RequestPredicates.GET("/hello"),
request -> ServerResponse.ok().body("Hello API")
)
.andRoute(
RequestPredicates.GET("/health"),
request -> ServerResponse.ok().body("OK")
)
);
}
nest 内部的路由会自动加上 /api 前缀进行匹配。实现上,nest 方法会将前缀谓词与每个嵌套路由的谓词做 and 组合,形成一个新的 RouterFunction。
源码分析:RouterFunctions.nest 方法 (org.springframework.web.servlet.function.RouterFunctions)
public static <T extends ServerResponse> RouterFunction<T> nest(
RequestPredicate predicate, RouterFunction<T> routerFunction) {
return new RouterFunction<T>() {
@Override
public Optional<HandlerFunction<T>> route(ServerRequest request) {
if (predicate.test(request)) {
return routerFunction.route(request);
}
return Optional.empty();
}
// ...
};
}
该方法创建了一个匿名 RouterFunction,只有当外部谓词(路径前缀)通过后,才委托给嵌套的 routerFunction 进行匹配。这体现了 装饰器模式,将路由按层分组,复用子路由。
与 @Controller 对应关系:nest 相当于类级 @RequestMapping("/api"),内部 route 相当于方法级 @GetMapping("/hello")。整个函数式 DSL 可以构建出任意深度的路由树。
3. 请求与响应:ServerRequest/ServerResponse 与消息转换器的深度协作
3.1 ServerRequest 的不可变设计与请求信息获取
ServerRequest 是对 HttpServletRequest 的不可变适配。每次请求到来时,RouterFunctionMapping 会通过 DefaultServerRequest 将原始的 HttpServletRequest 包装起来。接口提供了一系列惰性访问方法:
pathVariable(String name):提取路径变量,内部通过HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE获取。queryParam(String name):返回Optional<String>,读取查询参数。headers():返回ServerRequest.Headers,无感知访问所有请求头。body(Class<T>):利用HttpMessageConverter读取请求体并反序列化(详见 3.2)。servletRequest():回溯原生HttpServletRequest,以备特殊需求。
ServerRequest 保证了线程安全性,因为每次请求都创建新实例,不存在共享状态。
3.2 请求体转换:body() 方法与 HttpMessageConverter 集成
函数式端点没有 @RequestBody 注解,请求体到对象的转换通过 ServerRequest.body(Class<T>) 显式触发。其内部机制与我们第 4 篇文章(HTTP 消息转换器)所述的完全相同。
源码分析:DefaultServerRequest.body(Class<T>) (org.springframework.web.servlet.function.DefaultServerRequest)
@Override
public <T> T body(Class<T> clazz) throws ServletException, IOException {
return body(clazz, new HttpMessageConverterInitializer().register(clazz));
}
private <T> T body(TypeReference<T> typeReference, HttpMessageConverterInitializer initializer)
throws ServletException, IOException {
HttpInputMessage inputMessage = new ServletServerHttpRequest(servletRequest);
MediaType contentType = inputMessage.getHeaders().getContentType();
// 遍历所有注册的 HttpMessageConverter
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter.canRead(typeReference.getType(), contentType)) {
@SuppressWarnings("unchecked")
T result = (T) converter.read((Class)typeReference.getType(), inputMessage);
return result;
}
}
throw new HttpMediaTypeNotSupportedException("Content-Type not supported: " + contentType);
}
代码清晰地展示了函数式端点如何复用 HttpMessageConverter 链:从 ServletServerHttpRequest 中获取内容类型,遍历所有注册的 messageConverters(来自 WebMvcConfigurer 配置),调用匹配的转换器读取并反序列化。如果没有任何转换器支持该类型或 Content-Type,则抛出 HttpMediaTypeNotSupportedException,该异常将由 DispatcherServlet 的异常解析器统一处理。
泛型体支持:传递 new ParameterizedTypeReference<List<User>>() {} 可以处理泛型集合,内部利用 TypeReference 封装类型信息。
3.3 ServerResponse 的建造者模式与响应构建
ServerResponse 是不可变的响应抽象。创建它需要使用建造者:
ServerResponse.ok()
.header("X-Custom", "value")
.contentType(MediaType.APPLICATION_JSON)
.body(new User("John"));
建造者遵循流式接口,最终 body(Object) 或 build() 返回一个不可变的 ServerResponse 实现(通常是 DefaultServerResponse)。建造者内部同样借助 HttpMessageConverter 将 body 对象序列化为响应流。
源码分析:DefaultServerResponseBuilder.body(Object) (org.springframework.web.servlet.function.DefaultServerResponseBuilder)
public ServerResponse body(Object body) {
// 根据返回类型和请求 Accept 头选择最佳消息转换器
this.body = body;
return build();
}
public ServerResponse build() {
return new DefaultServerResponse(
statusCode, headers, cookies, body, messageConverters);
}
DefaultServerResponse 实现了 ServerResponse 的 writeTo 方法,在输出阶段调用消息转换器将 body 写入 HttpServletResponse:
@Override
public void writeTo(HttpServletResponse response, ...) {
if (body != null) {
MediaType selectedMediaType = selectMediaType(request);
for (HttpMessageConverter<?> converter : messageConverters) {
if (converter.canWrite(body.getClass(), selectedMediaType)) {
converter.write(body, selectedMediaType, new ServletServerHttpResponse(response));
return;
}
}
}
// ...
}
设计模式:建造者模式将复杂的响应构建过程(状态码、头、Cookie、主体)封装成步骤,最终产生原语不可变对象。策略模式体现在消息转换器的选择上。
3.4 底层适配:如何将响应写入 Servlet
最终,调用链回到 HandlerFunctionAdapter(详见第 4.4 节),它执行 handler.handle(request) 得到 ServerResponse,然后调用 response.writeTo(servletRequest, servletResponse, ...),将逻辑响应写入底层的 HttpServletResponse。这个适配层保证了函数式模型与 Servlet 容器的兼容。
4. 路由解析与挂载:RouterFunctionMapping 如何融入 DispatcherServlet
4.1 RouterFunctionMapping:标准 HandlerMapping 的实现
Spring 专门设计了 RouterFunctionMapping,使其成为 DispatcherServlet 众多 HandlerMapping 中的一个。它实现了 InitializingBean 接口,并内置 order 属性(默认值为 1)。注解的 RequestMappingHandlerMapping 默认 order=0。这意味着默认情况下,请求会优先被 RequestMappingHandlerMapping 匹配;只有当注解映射找不到时,RouterFunctionMapping 才会尝试匹配。这一顺序至关重要(详见事故案例 7.1)。
4.2 初始化扫描:收集所有 RouterFunction Bean
在容器启动阶段,RouterFunctionMapping.afterPropertiesSet() 被调用,它遍历 ApplicationContext 中所有 RouterFunction<?> 类型的 Bean,并通过 and 方法将它们组合成一个根 RouterFunction。
源码片段:RouterFunctionMapping.initRouterFunctions() (org.springframework.web.servlet.function.support.RouterFunctionMapping)
@Override
public void afterPropertiesSet() throws Exception {
// 从容器中收集所有 RouterFunction Bean
Map<String, RouterFunction<?>> beans = obtainApplicationContext()
.getBeansOfType(RouterFunction.class);
if (!beans.isEmpty()) {
this.routerFunction = beans.values().stream()
.reduce(RouterFunction::and)
.orElse(null);
}
// ... 打印日志
}
逐段解读:
- 获取所有类型为
RouterFunction的 Bean(通常由@Bean方法定义)。 - 使用 Stream 的
reduce操作,通过and方法将所有路由串联成一个复合体。and方法会创建一个新的RouterFunction,其route方法依次调用嵌套路由的route,返回第一个非空结果。 - 组合后的单一
routerFunction存储在实例变量中,作为后续匹配的统一入口。
这个过程没有扫描类路径,只是简单的 Bean 收集和组合,非常高效。
4.3 请求匹配:从 RouterFunction 映射到 HandlerFunction
当请求进入 DispatcherServlet 的 doDispatch 流程,它遍历所有 HandlerMapping(按 order 排序)。对于 RouterFunctionMapping,getHandler 方法调用如下:
源码片段:RouterFunctionMapping.getHandler(ServerRequest) (简化)
@Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
if (this.routerFunction != null) {
ServerRequest serverRequest = new DefaultServerRequest(request, this.messageConverters);
return this.routerFunction.route(serverRequest)
.map(handler -> (Object) handler)
.orElse(null);
}
return null;
}
核心逻辑:
- 将
HttpServletRequest封装为ServerRequest,传递消息转换器列表。 - 调用组合后
routerFunction的route方法。该方法内部会顺序测试每个RouterFunction的谓词,第一个匹配的返回其HandlerFunction。 - 如果找到,将
HandlerFunction作为 handler 返回;否则返回null,让其他HandlerMapping继续尝试。
这里返回的 handler 是一个 HandlerFunction 对象(Lambda 或方法引用),而不是 HandlerMethod。DispatcherServlet 得到 handler 后,会查询对应的 HandlerAdapter。
4.4 HandlerFunctionAdapter:调用函数式处理者的适配器
DispatcherServlet 需要将不同类型的 handler 分派给合适的适配器。HandlerFunctionAdapter 的 supports 方法简单检查 handler 是否为 HandlerFunction 的实例。
源码片段:HandlerFunctionAdapter.handle (org.springframework.web.servlet.function.support.HandlerFunctionAdapter)
@Override
public ModelAndView handle(HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
Object handler) throws Exception {
HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;
ServerRequest request = new DefaultServerRequest(servletRequest, this.messageConverters);
ServerResponse response = handlerFunction.handle(request);
return response.writeTo(servletRequest, servletResponse, new Context() {...});
}
解读:
- 直接强转为
HandlerFunction,然后调用handle(request)执行业务逻辑,得到ServerResponse。 response.writeTo(...)将逻辑响应写入底层响应流。这个过程利用了消息转换器(第 3 章),最终完成输出。- 函数的任何异常直接抛出,被外围
DispatcherServlet.processHandlerException捕获,进入标准异常解析链。
序列图:路由解析与请求处理
sequenceDiagram
participant Client
participant DispatcherServlet
participant HandlerMappingChain as HandlerMapping列表 (order排序)
participant RouterFunctionMapping
participant HandlerFunctionAdapter
participant RouterFunction
participant HandlerFunction
Client->>DispatcherServlet: HTTP Request
DispatcherServlet->>HandlerMappingChain: getHandler(request)
loop 遍历 HandlerMapping
HandlerMappingChain->>RouterFunctionMapping: getHandlerInternal(request)
RouterFunctionMapping->>RouterFunction: route(serverRequest)
RouterFunction-->>RouterFunctionMapping: Optional<HandlerFunction>
alt 匹配成功
RouterFunctionMapping-->>DispatcherServlet: HandlerFunction
else 未匹配
RouterFunctionMapping-->>DispatcherServlet: null
end
end
DispatcherServlet->>HandlerFunctionAdapter: supports(handler)?
HandlerFunctionAdapter-->>DispatcherServlet: true
DispatcherServlet->>HandlerFunctionAdapter: handle(request, response, handler)
HandlerFunctionAdapter->>HandlerFunction: handle(request)
HandlerFunction-->>HandlerFunctionAdapter: ServerResponse
HandlerFunctionAdapter->>ServerResponse: writeTo(servletRequest, servletResponse)
ServerResponse-->>DispatcherServlet: (完成写入)
DispatcherServlet-->>Client: HTTP Response
图表主旨概括:此序列图展示了从请求抵达 DispatcherServlet 到函数式端点被匹配、调用并返回响应的完整协作流程,清晰揭示了 RouterFunctionMapping 和 HandlerFunctionAdapter 作为桥梁的角色。
逐层/逐元素分解:
DispatcherServlet按照HandlerMapping的 order 顺序轮询,RouterFunctionMapping排在RequestMappingHandlerMapping之后(默认 order 1)。RouterFunctionMapping将所有组合好的RouterFunction拿出来,调用route进行谓词匹配,若找到则返回HandlerFunction对象。HandlerFunctionAdapter判断 handler 类型,执行函数调用,获得ServerResponse。ServerResponse.writeTo负责将响应数据序列化到HttpServletResponse。
设计原理映射:适配器模式将 HandlerFunction 这种接口适配到 DispatcherServlet 期望的 handler 处理流程中。RouterFunctionMapping 充当了桥接(Bridge)角色,将 RouterFunction 与 HandlerMapping 统一。
工程联系与关键结论:函数式端点的运行并不神奇,它严格遵守 Spring MVC 的 HandlerMapping-HandlerAdapter 协作合同,因此天然享有拦截器、异常解析器、消息转换器等全部基础设施。
5. 参数提取、校验与异常处理在函数式管道中的实现
5.1 路径变量与查询参数的函数式提取
函数式端点告别了 @PathVariable 和 @RequestParam,转而使用 ServerRequest 的方法:
RouterFunctions.route(GET("/users/{userId}"), request -> {
String userId = request.pathVariable("userId");
String type = request.queryParam("type").orElse("default");
// 使用 userId 和 type
return ServerResponse.ok().body(...);
});
路径变量底层来源于 HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE 属性,这是由 RouterFunctionMapping(或任何路径匹配的 HandlerMapping)在匹配时放入请求属性的。ServerRequest 只是提供了类型安全的访问器。这样,类型依赖从编译时注解转移到运行时调用,测试时可手动构建 ServerRequest 并注入这些属性。
5.2 手动校验:Validator 与 DataBinder 的集成实践
函数式端点没有 @Valid 和 @Validated 注解。如果需要 Bean Validation,必须显式调用 Validator:
@Bean
RouterFunction<ServerResponse> createUserRoute(Validator validator) {
return RouterFunctions.route(POST("/users"), request -> {
User user = request.body(User.class);
// 手动触发校验
DataBinder binder = new DataBinder(user);
binder.addValidators(validator);
binder.validate();
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.hasErrors()) {
// 构建错误响应
return ServerResponse.badRequest().body(bindingResult.getAllErrors());
}
// 执行业务逻辑
return ServerResponse.ok().build();
});
}
validator Bean 可以是 LocalValidatorFactoryBean,支持 JSR-380。尽管代码稍显冗长,但这种方式将校验过程显式化,测试时可以精确控制校验行为。部分团队会封装可复用的校验函数 validateAndProcess(Validator, Function<T, ServerResponse>),减少模板代码。
校验序列图
sequenceDiagram
participant HandlerFunction
participant ServerRequest
participant HttpMessageConverter
participant DataBinder
participant Validator
participant ServletResponse
HandlerFunction->>ServerRequest: body(User.class)
ServerRequest->>HttpMessageConverter: read(User.class, inputMessage)
HttpMessageConverter-->>ServerRequest: User对象
ServerRequest-->>HandlerFunction: User对象
HandlerFunction->>DataBinder: new DataBinder(user)
HandlerFunction->>DataBinder: addValidators(validator)
HandlerFunction->>DataBinder: validate()
DataBinder->>Validator: validate(user, errors)
Validator-->>DataBinder: 校验结果
alt 有错误
HandlerFunction->>ServerResponse: badRequest().body(errors)
else 无错误
HandlerFunction-->>HandlerFunction: 业务处理
HandlerFunction->>ServerResponse: ok().body(result)
end
HandlerFunction->>ServletResponse: writeTo
图表主旨概括:展示了函数式端点内请求体转换、手动校验及响应的流水线,强调了开发者对校验完全可控。
设计原理映射:DataBinder 作为桥梁,将 Validator 策略应用于目标对象,收集错误信息。函数式端点通过显式调用这些组件,避免了框架隐式切入,使数据流清晰可追踪。
关键结论:虽然失去了 @Valid 的声明式便利,但显式校验提供了更精细的掌控力,且符合函数式编程“显式优于隐式”的哲学。
5.3 异常处理:复用 HandlerExceptionResolver 体系
HandlerFunction 抛出的任何异常都会被 HandlerFunctionAdapter 传递到 DispatcherServlet,随后由 HandlerExceptionResolver 链处理(参见第 6 篇异常处理)。因此,在函数式端点中抛出的异常可以被 @ControllerAdvice 全局异常处理器捕获,前提是该 @ExceptionHandler 方法不依赖特定的 HandlerMethod(仅根据异常类型)。
示例:全局异常处理器同时处理函数式端点和注解端点
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
return ResponseEntity.badRequest().body("Invalid argument: " + ex.getMessage());
}
}
当函数式端点执行 throw new IllegalArgumentException("userId required") 时,上述方法会触发,返回 400 响应。这证明函数式端点享有完整的异常解析能力。然而,如果 @ExceptionHandler 方法签名包含了 WebRequest 或 HttpServletRequest 之外的 “特定于控制器的参数”(如 Model、RedirectAttributes),由于没有 HandlerMethod 提供这些信息,部分解析可能受限,但通常不影响纯 REST API 的错误处理。
函数式端点还可以直接在 HandlerFunction 内部使用 try-catch 构建 ServerResponse,绕过全局异常解析,这在轻量网关中常用。
6. 与 @Controller 注解模型的对比与抉择
6.1 多维度对比
| 维度 | WebMvc.fn 函数式端点 | @Controller 注解端点 |
|---|---|---|
| 路由定义方式 | RouterFunction 编程式组合 | @RequestMapping 声明式注解 |
| 启动性能 | 极高,无扫描,Bean 直接注册 | 需扫描类和方法,构建 HandlerMethod |
| 运行时开销 | 纯 Lambda 调用,无反射和代理 | 反射调用,可能涉及 AOP 代理 |
| 单元测试 | 极其简单,无 Spring 上下文 | 需要 MockMvc 或部分上下文加载 |
| AOP 支持 | 不支持方法级代理(Lambda 无法代理) | 完整支持事务、缓存、安全注解等 |
| 参数校验 | 显式调用 Validator,手动控制 | @Valid + BindingResult 自动 |
| Spring Security 注解 | 不支持 @PreAuthorize 等方法级安全 | 完全支持 |
| 视图解析 | 可返回视图名,但不常用 | 原生支持 ModelAndView、视图解析 |
| 文档生成 | 无注解,Swagger 等难以自动生成 | 通过注解可生成 OpenAPI 文档 |
| 动态路由 | 可在运行时动态组合/替换路由 | 静态,需编程修改映射表(较复杂) |
| 代码有机组织 | 所有相关路由可集中在一个 @Bean 方法 | 按类分散,但结构清晰 |
6.2 决策建议
-
优先选择函数式端点 的场景:
- 极简的 API 网关或边缘服务,要求快速启动和小体积。
- 需要动态下发路由规则的框架或平台。
- 团队偏好函数式风格,且使用轻量级安全模型(如通过拦截器或过滤实现)。
- 希望提升端点的单元测试效率,解除对 Spring 上下文的依赖。
-
优先选择注解端点 的场景:
- 复杂的业务系统,严重依赖声明式事务(
@Transactional)、缓存(@Cacheable)、安全(@PreAuthorize)。 - 需要自动生成 OpenAPI 文档(Swagger)。
- 开发者习惯注解约定,减少样板代码。
- 复杂的业务系统,严重依赖声明式事务(
6.3 混合使用的最佳实践
实际项目中两者可以同时存在。推荐策略:
- 查询类简单 API 使用函数式端点,命令类业务操作使用
@Controller。 - 将函数式端点定义为独立
@Configuration类中的@Bean,保持与注解控制器隔离。 - 留意
HandlerMapping的 order,必要时通过spring.webflux.functional.enabled或直接设置RouterFunctionMapping的 order 属性调整优先级,避免路由相互覆盖。
7. 生产事故排查专题
7.1 函数式路由优先级低于注解请求导致 404
现象:项目中同时使用了 @RestController 和 RouterFunction 定义的 /api/health 端点。奇怪的是,请求 /api/health 总是返回注解端点的结果,函数式端点似乎从未生效。当临时移除注解端点后,函数式端点正常工作。
排查思路:
- 检查
DispatcherServlet的HandlerMapping顺序,通过日志或 actuator 端点查看。 - 发现
RequestMappingHandlerMapping(order=0) 匹配了/api/health,并返回对应的HandlerMethod。 - 由于顺序问题,
RouterFunctionMapping(order=1) 没有被执行。
根因:两个 HandlerMapping 映射了相同的路径,默认顺序下注解端点优先。RouterFunctionMapping 在 RequestMappingHandlerMapping 之后,一个请求只要被先前的 HandlerMapping 处理,就不会再往下走。
解决:
- 方法一:在函数式端点的
RequestPredicate中使用更具体的谓词,但若注解已瓜分所有路径,此法无效。 - 方法二:调整
RouterFunctionMapping的 order,例如通过@Bean并设置setOrder(Ordered.HIGHEST_PRECEDENCE)使其优先于注解映射。但这样做会导致所有函数式端点优先匹配,可能影响其他路径,需谨慎。 - 方法三(推荐):保持同一路径只使用一种风格,或者将函数式端点定义为不同的路径前缀(如
/fn/api/health),避免冲突。
最佳实践:在混合使用的系统中,明确路由规划,使用不同路径前缀隔离;通过配置管理 HandlerMapping 的顺序,并编写集成测试验证路由行为。
事故序列图
sequenceDiagram
participant Client
participant DispatcherServlet
participant RequestMappingHandlerMapping as ReqMappingHM (order=0)
participant RouterFunctionMapping as RouterFnHM (order=1)
Client->>DispatcherServlet: GET /api/health
DispatcherServlet->>RequestMappingHandlerMapping: getHandler(request)
RequestMappingHandlerMapping-->>DispatcherServlet: HandlerMethod (匹配)
DispatcherServlet-->>Client: 返回注解端点的响应
Note right of RouterFunctionMapping: 从未被调用
关键结论:HandlerMapping 的顺序决定了相同路径下的优先级,不恰当的组合会导致功能静默失效,排查时需立刻审视 mapping 注册顺序。
7.2 函数式端点中 body() 方法不生效,总是返回空对象
现象:POST 请求发送 JSON 数据到函数式端点,request.body(User.class) 返回的 User 对象所有字段为 null,但请求体本身包含正确的 JSON。
排查思路:
- 检查请求 Content-Type 头,确认为
application/json。 - 检查日志,发现没有异常抛出,也没有消息转换失败的痕迹。
- 检查
User类是否有无参构造、getter/setter,确认默认 Jackson 可以反序列化。 - 检查项目中自定义
WebMvcConfigurer是否重写了configureMessageConverters方法,彻底替换了转换器列表,导致 Jackson 被移除。 - 发现项目中存在一个配置类,调用了
configureMessageConverters并只添加了一个自定义转换器,但该转换器不支持 JSON。
根因:configureMessageConverters 会完全替换 Spring Boot 自动配置的默认转换器列表(包括 MappingJackson2HttpMessageConverter),导致 JSON 转换能力消失。而 request.body() 会遍历列表,找不到支持 JSON 的转换器时,并不一定抛异常,如果恰好存在一个能处理 Object 但无法正确解析的转换器,就可能返回默认构造的空对象。
解决:应将自定义转换器通过 extendMessageConverters 添加,而非替换;或在 configureMessageConverters 中显式加入 Jackson 转换器。
最佳实践:永远使用 extendMessageConverters 扩展转换器,除非你非常确定要完全接管消息转换。对函数式端点编写单元测试时,必须模拟消息转换器,确保序列化/反序列化正确。
8. 面试高频专题
Q1: 什么是 Spring WebMvc.fn?它与传统的 @Controller 有什么不同?
- 标准回答:WebMvc.fn 是 Spring 5.2 引入的函数式 Web 端点定义模型,通过
RouterFunction、HandlerFunction和RequestPredicate替代@Controller和@RequestMapping等注解,以 Lambda 表达式定义路由和处理逻辑。它与注解模型共享相同的 Servlet 基础设施,但无反射、无代理,路由定义可编程组合。 - 追问1:函数式端点如何注册到 Spring 容器?答:通过
@Bean方法返回RouterFunction实例,RouterFunctionMapping自动收集所有RouterFunctionBean 并注册。 - 追问2:注解和函数式端点哪个性能高?答:函数式端点运行时开销略低(无反射),但核心性能差异通常在 I/O 和业务逻辑,而非调度层。
- 加分回答:函数式端点特别适合云原生环境,因为镜像体积更小,启动更快;支持 GraalVM Native Image 编译,没有反射能更好地静态分析。
Q2: RouterFunction 是如何工作的?它如何与 DispatcherServlet 配合?
- 标准回答:
RouterFunction是个函数接口,route(ServerRequest)返回Optional<HandlerFunction>。RouterFunctionMapping将其包装为HandlerMapping,在DispatcherServlet调度时收集所有RouterFunctionBean,组合后顺序匹配请求,返回第一个匹配的HandlerFunction。HandlerFunctionAdapter负责执行该函数。 - 追问1:如果多个 RouterFunction 匹配同一个请求怎么办?答:按
and组合的顺序,第一个匹配的返回,后面的不再尝试。 - 追问2:RouterFunctionMapping 的 order 默认值是多少?答:默认为 1,而 RequestMappingHandlerMapping 为 0,因此注解端点优先。
- 追问3:能否动态修改 RouterFunction?答:可以重新组合新的 RouterFunction,利用编程式的
and/nest并在配置刷新时替换 Bean,或者定义能根据外部状态动态决策的 RouterFunction。
Q3: 函数式端点如何处理参数(如 @RequestParam 和 @RequestBody)?
- 标准回答:使用
ServerRequest.pathVariable()、ServerRequest.queryParam()和ServerRequest.body(Class)。body()内部使用HttpMessageConverter读取请求体,等价于@RequestBody。没有注解,所有提取显式调用。 - 追问1:如何处理 Multipart 文件上传?答:
ServerRequest.servletRequest()获取原生HttpServletRequest,然后使用 Spring 的MultipartFile解析或者直接通过request.body(Class)需要注册支持 multipart 的转换器。 - 追问2:如何提取 Cookie 和 Header?答:
request.headers()或request.cookies()提供便捷方法。 - 加分回答:由于显式提取,测试时可以构建模拟的
ServerRequest,不需要 MockMvc。
Q4: 如何在函数式端点中实现请求校验?
- 标准回答:手动使用
Validator和DataBinder。注入ValidatorBean,创建DataBinder绑定对象,调用validate(),根据BindingResult决定是否返回错误响应。 - 追问1:能不能使用 @Valid 注解?答:不能,因为无代理。但可以编写可复用的校验包装函数。
- 追问2:全局异常处理器能捕获校验异常吗?答:可以,父异常如
MethodArgumentNotValidException不能直接抛,但可以自定义异常并让@ControllerAdvice处理。 - 加分回答:可封装
validate工具方法,采用 “要么成功要么抛异常” 的单子模式处理校验结果。
Q5: 函数式端点能否使用 Spring Security 的注解进行权限控制?为什么?
- 标准回答:不能。
@PreAuthorize、@Secured等依赖于 Spring AOP 代理,而HandlerFunction是 Lambda 表达式,无法被代理。可以通过显式编码使用SecurityContextHolder,或通过RouterFunction的filter方法进行安全拦截。 - 追问1:filter 方法怎么用?答:
RouterFunction.filter(HandlerFilterFunction)在调用 handler 前后执行,可检查权限,拒绝时返回错误响应。 - 追问2:方法级安全在函数式端点有什么替代方案?答:可以在 filter 中手工进行
@PreAuthorize的 SpEL 计算,但复杂度过高,推荐在服务层保障安全。 - 加分回答:若希望在函数式端点中获得声明式安全,可以结合
HandlerFilterFunction和自定义注解,但框架不直接支持。
Q6: HandlerFunctionAdapter 的作用是什么?为什么需要它?
- 标准回答:
DispatcherServlet依赖HandlerAdapter模式调用不同类型的 handler。HandlerFunctionAdapter检查 handler 是否为HandlerFunction实例,若是则执行handle(request)获得ServerResponse,并将其写入HttpServletResponse。它将函数式端点适配进标准 MVC 流程。 - 追问1:为什么不在 RouterFunctionMapping 中直接调用?答:遵循 MVC 骨架的责任分离,
HandlerMapping只负责查找,HandlerAdapter负责执行,使扩展更灵活。 - 追问2:函数式端点的异常是如何被捕获的?答:
HandlerFunctionAdapter直接抛出异常,DispatcherServlet的processHandlerException会调用HandlerExceptionResolver链处理。 - 加分回答:如果你自定义了一种 handler 类型,只需提供相应的
HandlerAdapter即可融入。
Q7: 多个 RouterFunction 同时存在时,它们的匹配顺序是怎样的?如何控制?
- 标准回答:所有
RouterFunctionBean 通过and组合成一个,按 Bean 收集的顺序(通常为 Spring 容器定义顺序)排列。匹配时依次尝试,第一个匹配的返回。可以通过@Order或@Priority注解影响 Bean 的收集顺序,或者将所有路由集中一个@Bean方法内显式控制。 - 追问1:如何调整两个独立 @Bean 方法的顺序?答:在组合流中使用
reduce会保持集合顺序;若需要明确顺序,可使用@Order注解在RouterFunction定义上。RouterFunctionMapping使用ListableBeanFactory获取时,默认排序不保证;可依赖@Order和Ordered接口。 - 追问2:能否在运行时动态改变顺序?答:可以,重新构建一个组合
RouterFunction并替换RouterFunctionMapping中的 Bean,但通常不推荐。 - 加分回答:大批量路由时,可自定义实现
RouterFunction,内部使用哈希表进行 O(1) 路径匹配,提升性能。
Q8: 函数式端点和注解式端点可以共存吗?如果共存,请求优先匹配谁?
- 标准回答:可以共存。默认情况下,
RequestMappingHandlerMapping(order=0) 优先于RouterFunctionMapping(order=1),所以注解端点会先匹配,函数式端点作为后备。若需反转,可设置RouterFunctionMapping的 order 为Ordered.HIGHEST_PRECEDENCE。 - 追问1:如果两个风格定义了完全相同的路径,会发生什么?答:谁优先返回 handler 谁处理,另一个被屏蔽。务必避免路径冲突。
- 追问2:Spring Boot 如何自动配置这两种映射?答:
WebMvcAutoConfiguration会注册RequestMappingHandlerMapping,而RouterFunctionMapping由WebMvcAutoConfiguration或RouterFunctionAutoConfiguration自动配置。 - 加分回答:通过
spring.factories可剔除自动配置的RouterFunctionMapping,完全回归注解。
Q9: 为什么要引入 WebMvc.fn?它解决了注解模型的什么痛点?
- 标准回答:主要解决启动性能、内存占用和测试复杂性。注解模型需要类路径扫描和反射,开发胖 jar 时启动慢;函数式端点 Bean 显式注册,无扫描、无反射、无代理,天然适合 GraalVM Native Image 编译。同时,测试可脱离 Spring 容器进行纯函数单元测试。
- 追问1:它真的是为了替换注解吗?答:不,是为了提供另一种选择,尤其针对函数式风格和云原生优化。
- 追问2:有没有计划废弃注解?答:没有,Spring 官方明确两者长期共存。
Q10: 函数式端点相比注解端点,在单元测试方面有什么优势?
- 标准回答:可以直接实例化
DefaultServerRequest或使用 mock 构造请求,调用HandlerFunction.handle(request)获取ServerResponse,无需启动 Spring 容器或 MockMvc。这使得测试速度极快,且依赖清晰。 - 追问1:如何构造 ServerRequest?答:使用
ServerRequest.create(method, uri, ...)或MockServerRequest(Spring 测试模块提供)。 - 追问2:测试中如何模拟消息转换器?答:传递一个自定义的
HttpMessageConverter列表给DefaultServerRequest或使用MockServerRequest构建。 - 加分回答:可编写属性驱动的测试,将
ServerRequest构造参数化,覆盖大量边界条件。
Q11: 对于大文件上传(Multipart),函数式端点是如何处理的?
- 标准回答:
ServerRequest.servletRequest()获取HttpServletRequest,然后通过 Spring 的MultipartResolver解析 multipart。或者使用ServerRequest.body(MultipartFile.class)(需要引入 multipart 消息转换器,但默认不支持)。常规做法是在RouterFunction的处理器中获取HttpServletRequest,手动解析MultipartHttpServletRequest。 - 追问1:有没有更函数式的方式?答:可封装一个
ServerRequest装饰器,提供multipartData()方法,内部解析。 - 追问2:如何限制上传大小?答:通过
spring.servlet.multipart.max-file-size配置,与注解端点一致。 - 加分回答:对于流式上传,可直接使用
request.servletRequest().getInputStream()进行分块处理,不经过消息转换器。
Q12: (系统设计题)设计一个基于 WebMvc.fn 的轻量级 API 网关,要求支持从配置中心动态下发路由规则,并能够根据请求头中的版本号将请求转发到不同的内部处理链。请给出路由函数的核心设计,并描述动态刷新路由的机制。
-
回答要点:
- 路由设计:定义一个顶层
RouterFunction,它不固定andRoute,而是内部根据版本号动态分发。实现一个DynamicRouterFunction,其route方法读取ServerRequest的X-API-Version头,查找映射表,将请求委托给对应的HandlerFunction(内部处理链)。映射表Map<Integer, HandlerFunction<ServerResponse>>通过配置中心动态维护。 - 动态刷新:使用应用内事件或定期轮询配置中心。当配置变化时,构建新的映射表,并原子替换
DynamicRouterFunction内部的引用(AtomicReference)。或者将RouterFunctionBean 的作用域定义为@RefreshScope,结合 Spring Cloud Config 自动刷新。 - 优点:无需重启,可热更新路由;纯函数式,测试容易。
- 详细描述:设计一个
GatewayRouterFunction实现RouterFunction<ServerResponse>,持有AtomicReference<Map<Integer, HandlerFunction<ServerResponse>>>。route方法提取版本头,若匹配则调用对应 handler,否则返回 400。监听配置变更,更新 map。可将此 RouterFunction 暴露为 Bean。
- 路由设计:定义一个顶层
-
追问1:如何确保线程安全?答:
AtomicReference或CopyOnWriteMap保证发布安全,route方法无副作用,天然线程安全。 -
追问2:如果版本对应的处理链内部也需要动态路由怎么办?答:处理链同样可设计为
RouterFunction,进一步嵌套。 -
追问3:如何处理版本不存在的请求?答:返回预定义的
HandlerFunction,返回 400 或降级到默认版本。 -
加分回答:结合 Spring Cloud Gateway 的思想,引入过滤器链,函数式网关可轻量定制路由规则和限流。
附录:WebMvc.fn 核心接口速查表
| 接口/类 | 职责 | 关键方法 | 对应 MVC 概念 |
|---|---|---|---|
RouterFunction<T> | 将请求映射到处理函数 | route(ServerRequest): Optional<HandlerFunction<T>> | HandlerMapping |
HandlerFunction<T> | 处理请求,返回响应 | handle(ServerRequest): T | @Controller 方法 |
RequestPredicate | 请求匹配谓词 | test(ServerRequest): boolean | @RequestMapping 属性 |
RouterFunctions | 静态辅助工厂 | route(), nest(), filter() | 路由 DSL |
RequestPredicates | 谓词工厂 | GET(path), POST(path), contentType(), path() 等 | 请求条件 |
ServerRequest | 不可变服务端请求抽象 | body(), pathVariable(), queryParam(), headers() | HttpServletRequest + 参数解析 |
ServerResponse | 不可变服务端响应抽象 | ok(), created(), badRequest() 等建造者方法 | ResponseEntity / 直接写 HttpServletResponse |
RouterFunctionMapping | HandlerMapping 实现,集成函数式路由 | getHandlerInternal() | RequestMappingHandlerMapping |
HandlerFunctionAdapter | 调用 HandlerFunction 的适配器 | handle() | RequestMappingHandlerAdapter |
延伸阅读
- Spring Framework 官方文档 - “Web on Servlet Stack” - “Functional Endpoints”
- 《Spring 实战(第 5 版)》相关章节
- Spring 源码分析系列:
RouterFunctionMapping与HandlerFunctionAdapter实现剖析
全文完。本文深入剖析了 WebMvc.fn 函数式端点的架构与实现,通过与 Spring MVC 基础设施的深度整合,为函数式路由提供了与注解模型同等健壮的生态,却带来了轻量与可组合的全新可能性。在实际抉择时,理解两种模型的共存与协作,方能发挥 Spring Web 层的最大潜力。