在上一讲中,我们把 IoC 容器的内部运作拆解了一遍。现在你知道了,Spring 是如何管理 Bean、如何解决循环依赖的。但你每天接触最多的,其实是另一个东西------Spring MVC。
你写 @RestController、@GetMapping、@RequestParam,这些注解每天都在用。但你可能没想过:@GetMapping 标注的方法,Spring 是怎么找到它的?@RequestParam 里的参数名,Spring 是怎么从 HTTP 请求里取出来的?你 return 一个对象,Spring 又是怎么把它变成 JSON 的?
这所有的"魔法",都集中在 Spring MVC 的一个核心类里------DispatcherServlet。
这一篇,我们把 DispatcherServlet 从初始化到请求处理的完整链路走一遍。
学习目标
- 深入理解
DispatcherServlet在 Spring MVC 中的 "总指挥"角色 - 掌握
HandlerMapping、HandlerAdapter、ViewResolver的核心职责 - 理解 Spring MVC 的九大组件及其初始化时机
- 掌握
@ControllerAdvice的统一处理机制(数据返回、异常处理)
正文
一、Spring MVC 架构全景:DispatcherServlet 与九大组件
Spring MVC 的设计围绕 前端控制器模式(Front Controller Pattern) 展开。DispatcherServlet 就是这个前端控制器------所有请求都先经过它,由它分发给具体的处理器,最后再把结果返回给客户端。
DispatcherServlet 本身是一个 Servlet,它继承自 HttpServlet。当 Web 容器(如 Tomcat)启动时,会初始化 DispatcherServlet,而 DispatcherServlet 在初始化时会做一件关键的事情------初始化九大组件。
这九大组件是 Spring MVC 的"战略组件",各自承担不同的职责。它们的初始化入口在 DispatcherServlet 的 initStrategies() 方法中:
java
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context); // 文件上传解析器
initLocaleResolver(context); // 国际化解析器
initThemeResolver(context); // 主题解析器
initHandlerMappings(context); // 处理器映射器
initHandlerAdapters(context); // 处理器适配器
initHandlerExceptionResolvers(context); // 异常处理器
initRequestToViewNameTranslator(context); // 默认视图名转换器
initViewResolvers(context); // 视图解析器
initFlashMapManager(context); // Flash 属性管理器
}
九大组件的核心职责:
| 组件 | 职责 |
|---|---|
HandlerMapping |
根据请求找到对应的 Handler(处理器)和拦截器链 |
HandlerAdapter |
适配不同类型的 Handler,统一调用 |
HandlerExceptionResolver |
处理请求处理过程中抛出的异常 |
ViewResolver |
将逻辑视图名解析为真正的 View 对象 |
RequestToViewNameTranslator |
当处理器没有返回视图名时,从请求中提取默认视图名 |
LocaleResolver |
从请求中解析出 Locale(区域信息),用于国际化 |
ThemeResolver |
解析主题(Theme),用于页面样式切换 |
MultipartResolver |
处理文件上传请求的解析 |
FlashMapManager |
管理 Flash 属性(重定向时传递临时数据) |
其中,HandlerMapping、HandlerAdapter、ViewResolver 是最核心的三个,我们逐个展开。
二、HandlerMapping:URL 到 Controller 的映射器
当一个 HTTP 请求到达 DispatcherServlet 时,第一件事就是------找到谁来处理这个请求。
HandlerMapping 的职责就是:根据请求的 URL(以及请求方法、参数等)找到对应的 Handler。
Handler 可以理解为"能处理请求的东西"------在 Spring MVC 中,最常见的就是 @RequestMapping 标注的 Controller 方法。
Spring MVC 提供了多个 HandlerMapping 实现:
RequestMappingHandlerMapping(最常用):解析@RequestMapping及其衍生注解(@GetMapping、@PostMapping等),建立 URL → 方法的映射关系BeanNameUrlHandlerMapping:将 Bean 的名称映射为 URLSimpleUrlHandlerMapping:通过配置文件显式指定 URL 和 Handler 的映射
RequestMappingHandlerMapping 在容器启动时会扫描所有 @Controller 类,解析其中的 @RequestMapping 注解,构建一个URL 到 HandlerMethod 的映射表。当请求到来时,它在这个映射表中查找匹配的条目。
HandlerMapping 的返回值不是一个单独的 Handler,而是一个 HandlerExecutionChain ------它包含了 Handler 本身,以及所有需要在该请求上执行的拦截器(HandlerInterceptor)。
三、HandlerAdapter:适配器模式让不同类型的 Handler 统一执行
找到 Handler 之后,DispatcherServlet 并不能直接调用它------因为 Handler 的形式可能千差万别:
- 可能是
@RequestMapping标注的 Controller 方法 - 可能是实现了
Controller接口的类(Spring 2.5 之前的写法) - 可能是实现了
HttpRequestHandler接口的类
这些 Handler 的调用方式完全不同。DispatcherServlet 需要一种统一的方式 来调用它们------这就是 HandlerAdapter 的职责。
HandlerAdapter 接口定义了三个方法:
java
public interface HandlerAdapter {
// 判断当前适配器是否支持这个 Handler
boolean supports(Object handler);
// 执行 Handler,返回 ModelAndView
ModelAndView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception;
// 获取请求的最后修改时间(用于缓存)
long getLastModified(HttpServletRequest request, Object handler);
}
对于 @RequestMapping 类型的 Handler,对应的适配器是 RequestMappingHandlerAdapter。它做的事情包括:
- 解析方法参数(通过
HandlerMethodArgumentResolver) - 通过反射调用 Controller 方法
- 处理方法返回值(通过
HandlerMethodReturnValueHandler)
适配器模式的价值 :DispatcherServlet 不需要知道 Handler 的具体类型,它只需要拿到一个 HandlerAdapter,调用 handle() 方法即可。新增一种 Handler 类型,只需要新增对应的适配器------完全符合开闭原则。
四、参数解析与返回值处理:HandlerMethodArgumentResolver 和 HandlerMethodReturnValueHandler
RequestMappingHandlerAdapter 之所以强大,很大程度上是因为它内置了参数解析器 和返回值处理器两套体系。
参数解析器:HandlerMethodArgumentResolver
Controller 方法的参数五花八门:@RequestParam、@PathVariable、@RequestBody、@ModelAttribute、HttpServletRequest......每一种参数都需要从请求中提取并转换成对应的 Java 类型。
HandlerMethodArgumentResolver 就是处理这个事情的策略接口。它有两个核心方法:
java
public interface HandlerMethodArgumentResolver {
// 判断当前解析器是否支持这个参数
boolean supportsParameter(MethodParameter parameter);
// 从请求中解析出参数值
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
throws Exception;
}
Spring MVC 提供了大量的默认实现:
| 注解/参数类型 | 对应解析器 | 数据来源 |
|---|---|---|
@RequestParam |
RequestParamMethodArgumentResolver |
查询参数 / 表单参数 |
@PathVariable |
PathVariableMethodArgumentResolver |
URL 路径占位符 |
@RequestBody |
RequestResponseBodyMethodProcessor |
HTTP 请求体(JSON/XML) |
@ModelAttribute |
ModelAttributeMethodProcessor |
表单参数封装为对象 |
HttpServletRequest |
ServletRequestMethodArgumentResolver |
直接注入原生 Request 对象 |
RequestMappingHandlerAdapter 在初始化时会注册所有这些默认解析器。执行 Controller 方法时,它会遍历方法的每个参数,找到第一个 supportsParameter 返回 true 的解析器,调用 resolveArgument 解析出参数值。
返回值处理器:HandlerMethodReturnValueHandler
Controller 方法执行完后,返回值也需要处理。不同的返回类型对应不同的处理方式:
- 返回
String:可能是逻辑视图名,也可能是@ResponseBody的字符串 - 返回
ModelAndView:直接使用 - 返回 POJO(带
@ResponseBody):序列化为 JSON - 返回
ResponseEntity<T>:可自定义状态码和响应头
HandlerMethodReturnValueHandler 的策略接口和参数解析器类似,也包含 supportsReturnType 和 handleReturnValue 两个方法。
五、@ControllerAdvice 的奥秘:为什么它能统一处理异常和返回值
@ControllerAdvice 是 Spring MVC 中实现全局横切逻辑 的核心注解。它本质上是一个特殊的 @Component,在容器启动时会被扫描并注册。
@ControllerAdvice 主要配合三个注解使用:
1. @ExceptionHandler:全局异常处理
当 Controller 方法抛出异常时,DispatcherServlet 会委托给 HandlerExceptionResolver 处理。其中最重要的实现是 ExceptionHandlerExceptionResolver。
这个解析器在初始化时会扫描所有 @ControllerAdvice 类,收集其中标注了 @ExceptionHandler 的方法,建立异常类型 → 处理方法的映射缓存。当异常发生时,它会根据异常类型找到对应的处理方法并执行。
2. @ModelAttribute:全局数据绑定
在 @ControllerAdvice 类中,标注了 @ModelAttribute 的方法会在每个 Controller 方法执行之前被调用,返回的数据会自动添加到 Model 中,供视图渲染使用。
3. @InitBinder:全局数据绑定定制
用于初始化 WebDataBinder,可以定制参数绑定、数据校验等行为。
@RestControllerAdvice 是 @ControllerAdvice + @ResponseBody 的组合,专门用于 RESTful 场景------异常处理的结果会直接以 JSON 形式返回,而不是渲染为视图。
六、请求处理的完整流程:从 doDispatch 到响应返回
现在我们把整个流程串起来,看 DispatcherServlet 的 doDispatch 方法是如何一步步处理请求的:
① 请求进入 DispatcherServlet
↓
② getHandler():通过 HandlerMapping 找到 HandlerExecutionChain
(包含 Controller 方法和拦截器链)
↓
③ getHandlerAdapter():根据 Handler 类型获取对应的 HandlerAdapter
↓
④ 执行拦截器链的 preHandle()
↓
⑤ ha.handle():HandlerAdapter 执行 Controller 方法
├── 参数解析(HandlerMethodArgumentResolver)
├── 反射调用 Controller 方法
└── 返回值处理(HandlerMethodReturnValueHandler)
↓
⑥ 执行拦截器链的 postHandle()
↓
⑦ processDispatchResult():处理最终结果
├── 异常处理(如有异常,调用 HandlerExceptionResolver)
├── 视图解析(ViewResolver 将逻辑视图名解析为 View)
└── 视图渲染(View.render())
↓
⑧ 执行拦截器链的 afterCompletion()
↓
⑨ 响应返回客户端
代码示例
示例一:自定义参数解析器------让 Controller 直接接收当前登录用户
假设你有一个场景:很多接口都需要当前登录用户的信息。你不想在每个 Controller 方法里都去解析 Token、查数据库,而是希望直接在参数中拿到 User 对象。
第一步:创建自定义注解
java
package com.example.demo.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
// 不需要属性,仅作为标记
}
第二步:实现 HandlerMethodArgumentResolver
java
package com.example.demo.resolver;
import com.example.demo.annotation.CurrentUser;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
private final UserService userService;
public CurrentUserArgumentResolver(UserService userService) {
this.userService = userService;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 判断:参数是否标注了 @CurrentUser,并且类型是 User
return parameter.hasParameterAnnotation(CurrentUser.class)
&& parameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
// 从请求头中获取 Token
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String token = request.getHeader("Authorization");
// 解析 Token 获取用户 ID(实际项目中使用 JWT 工具类)
Long userId = parseToken(token);
// 查询用户信息并返回
return userService.findById(userId);
}
private Long parseToken(String token) {
// 简化的 Token 解析逻辑,实际应使用 JWT 解析
// 这里仅作演示
return 1L;
}
}
第三步:注册自定义参数解析器
java
package com.example.demo.config;
import com.example.demo.resolver.CurrentUserArgumentResolver;
import com.example.demo.service.UserService;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final UserService userService;
public WebConfig(UserService userService) {
this.userService = userService;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver(userService));
}
}
第四步:在 Controller 中使用
java
package com.example.demo.controller;
import com.example.demo.annotation.CurrentUser;
import com.example.demo.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/profile")
public User profile(@CurrentUser User user) {
// 直接拿到当前登录用户,无需手动解析 Token
return user;
}
}
关键观察 :HandlerMethodArgumentResolver 的扩展机制让 Spring MVC 的参数绑定变得极其灵活。你可以在 supportsParameter 中定义任意匹配规则,在 resolveArgument 中实现任意解析逻辑。
示例二:统一响应格式封装(ResponseBodyAdvice)
在前后端分离项目中,通常要求所有接口返回统一的 JSON 格式,比如:
json
{
"code": 0,
"message": "success",
"data": { ... }
}
用 ResponseBodyAdvice 可以统一实现这个封装,而不需要在每个 Controller 方法里手动构造。
第一步:定义统一响应类
java
package com.example.demo.common;
public class Result<T> {
private int code;
private String message;
private T data;
public Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Result<T> success(T data) {
return new Result<>(0, "success", data);
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
// getter / setter 省略
}
第二步:实现 ResponseBodyAdvice
java
package com.example.demo.advice;
import com.example.demo.common.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@RestControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 对所有带有 @ResponseBody 的返回值生效
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// 如果返回值已经是 Result 类型,不再重复包装
if (body instanceof Result) {
return body;
}
// 如果返回的是 String 类型,需要特殊处理
// 因为 StringHttpMessageConverter 要求返回值是 String
if (body instanceof String) {
return Result.success(body);
}
return Result.success(body);
}
}
第三步:在 Controller 中使用
java
@RestController
public class TestController {
@GetMapping("/user")
public User getUser() {
return new User(1L, "张三");
}
}
请求 /user 返回:
json
{
"code": 0,
"message": "success",
"data": {
"id": 1,
"name": "张三"
}
}
关键点 :ResponseBodyAdvice 在 HttpMessageConverter 序列化之前介入,可以对返回值做统一的预处理。注意 String 类型的特殊处理------StringHttpMessageConverter 要求返回值必须是 String,如果直接返回 Result 对象会抛出类型转换异常。
新手错误 vs 正确姿势
| 错误表象 | 根本原因 | 正确姿势 |
|---|---|---|
@Controller 和 @RestController 混淆,返回了 ModelAndView 但期望返回 JSON |
@RestController = @Controller + @ResponseBody,前者每个方法都自动带 @ResponseBody |
需要返回 JSON 用 @RestController;需要返回视图用 @Controller |
| 全局异常处理不生效,异常直接被容器处理 | @ControllerAdvice 类未被 Spring 扫描,或未被 @ExceptionHandler 正确配置 |
确保 @ControllerAdvice 类在 @ComponentScan 的扫描路径下,方法上标注 @ExceptionHandler |
@RequestBody 接收 JSON 时报 415 Unsupported Media Type |
请求头缺少 Content-Type: application/json,或未引入 Jackson 依赖 |
前端设置正确的 Content-Type,后端确保 jackson-databind 在 classpath 中 |
自定义 HandlerMethodArgumentResolver 不生效 |
未通过 WebMvcConfigurer.addArgumentResolvers() 注册 |
创建配置类实现 WebMvcConfigurer,在 addArgumentResolvers 中注册自定义解析器 |
ResponseBodyAdvice 对 String 类型返回值失效,报类型转换错误 |
StringHttpMessageConverter 要求返回值必须是 String 类型 |
在 beforeBodyWrite 中对 String 类型做特殊处理,返回 Result 的 JSON 字符串 |
疑难深度追问
Q1:DispatcherServlet 的 doDispatch 方法中,为什么要把"执行拦截器"放在"获取 HandlerAdapter"之后?
因为拦截器链(HandlerExecutionChain)是在 getHandler() 阶段获取的,其中包含了 Handler 对象。getHandlerAdapter() 需要根据 Handler 的类型来选择适配器。如果先执行拦截器,再获取适配器,逻辑上也没有问题------但 Spring 的设计是先准备好所有执行所需的对象(Handler 和 Adapter),再进入拦截器链的执行 ,这样职责更清晰。实际执行顺序是:getHandler() → getHandlerAdapter() → applyPreHandle() → ha.handle() → applyPostHandle()。
Q2:@ControllerAdvice 中的方法是如何被找到并执行的?
ExceptionHandlerExceptionResolver 在初始化时会调用 initExceptionHandlerAdviceCache() 方法,扫描所有 @ControllerAdvice 类,解析其中的 @ExceptionHandler 方法,建立异常类型 → 处理方法 的映射缓存。当 Controller 方法抛出异常时,DispatcherServlet 会遍历 HandlerExceptionResolver 列表,ExceptionHandlerExceptionResolver 会根据异常类型在缓存中查找匹配的处理方法,找到后通过反射执行。
Q3:如果多个 @ControllerAdvice 同时存在,它们的执行顺序如何确定?
通过 @Order 注解或实现 Ordered 接口来控制顺序。数字越小,优先级越高。如果没有指定,则按 Spring 默认的顺序(通常是加载顺序)执行。ExceptionHandlerExceptionResolver 在处理异常时会按优先级顺序查找匹配的 @ExceptionHandler 方法。
思考与延伸
-
动手验证 :在
doDispatch的各个关键节点(getHandler、applyPreHandle、ha.handle、applyPostHandle)打断点,用 Debug 模式启动一个 Spring Boot 应用,发送一个请求,观察整个调用链的执行顺序。 -
思考题 :
HandlerInterceptor的postHandle在什么情况下不会被执行?(提示:preHandle返回false,或 Controller 方法抛出异常且未被@ExceptionHandler捕获) -
延伸阅读 :Spring 官方文档中 "Web on Servlet Stack" 章节对
DispatcherServlet和九大组件有详细的说明。另外,RequestMappingHandlerAdapter的源码是理解参数解析和返回值处理的最佳入口。
参考与延伸阅读
- Spring Framework. Web on Servlet Stack --- DispatcherServlet. Spring Framework Documentation, 6.0.x
- Spring Framework. org.springframework.web.servlet Class DispatcherServlet. Spring Framework Javadoc
- Spring Framework. Exceptions --- HandlerExceptionResolver. Spring Framework Documentation
- 阿里云开发者社区. Spring MVC 的核心九大组件. 2022-12-28
- 腾讯云. SpringMVC源码系列:九大组件小记. 2025-06-07
- CSDN. DispatcherServlet 内部流程. 2025-06-10