第11篇:Spring MVC深入:从DispatcherServlet到ViewResolver的完整链路

在上一讲中,我们把 IoC 容器的内部运作拆解了一遍。现在你知道了,Spring 是如何管理 Bean、如何解决循环依赖的。但你每天接触最多的,其实是另一个东西------Spring MVC

你写 @RestController@GetMapping@RequestParam,这些注解每天都在用。但你可能没想过:@GetMapping 标注的方法,Spring 是怎么找到它的?@RequestParam 里的参数名,Spring 是怎么从 HTTP 请求里取出来的?你 return 一个对象,Spring 又是怎么把它变成 JSON 的?

这所有的"魔法",都集中在 Spring MVC 的一个核心类里------DispatcherServlet

这一篇,我们把 DispatcherServlet 从初始化到请求处理的完整链路走一遍。

学习目标

  • 深入理解 DispatcherServlet 在 Spring MVC 中的 "总指挥"角色
  • 掌握 HandlerMappingHandlerAdapterViewResolver 的核心职责
  • 理解 Spring MVC 的九大组件及其初始化时机
  • 掌握 @ControllerAdvice 的统一处理机制(数据返回、异常处理)

正文

一、Spring MVC 架构全景:DispatcherServlet 与九大组件

Spring MVC 的设计围绕 前端控制器模式(Front Controller Pattern) 展开。DispatcherServlet 就是这个前端控制器------所有请求都先经过它,由它分发给具体的处理器,最后再把结果返回给客户端。

DispatcherServlet 本身是一个 Servlet,它继承自 HttpServlet。当 Web 容器(如 Tomcat)启动时,会初始化 DispatcherServlet,而 DispatcherServlet 在初始化时会做一件关键的事情------初始化九大组件

这九大组件是 Spring MVC 的"战略组件",各自承担不同的职责。它们的初始化入口在 DispatcherServletinitStrategies() 方法中:

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 属性(重定向时传递临时数据)

其中,HandlerMappingHandlerAdapterViewResolver 是最核心的三个,我们逐个展开。

二、HandlerMapping:URL 到 Controller 的映射器

当一个 HTTP 请求到达 DispatcherServlet 时,第一件事就是------找到谁来处理这个请求

HandlerMapping 的职责就是:根据请求的 URL(以及请求方法、参数等)找到对应的 Handler

Handler 可以理解为"能处理请求的东西"------在 Spring MVC 中,最常见的就是 @RequestMapping 标注的 Controller 方法。

Spring MVC 提供了多个 HandlerMapping 实现:

  • RequestMappingHandlerMapping (最常用):解析 @RequestMapping 及其衍生注解(@GetMapping@PostMapping 等),建立 URL → 方法的映射关系
  • BeanNameUrlHandlerMapping:将 Bean 的名称映射为 URL
  • SimpleUrlHandlerMapping:通过配置文件显式指定 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。它做的事情包括:

  1. 解析方法参数(通过 HandlerMethodArgumentResolver
  2. 通过反射调用 Controller 方法
  3. 处理方法返回值(通过 HandlerMethodReturnValueHandler

适配器模式的价值DispatcherServlet 不需要知道 Handler 的具体类型,它只需要拿到一个 HandlerAdapter,调用 handle() 方法即可。新增一种 Handler 类型,只需要新增对应的适配器------完全符合开闭原则。

四、参数解析与返回值处理:HandlerMethodArgumentResolver 和 HandlerMethodReturnValueHandler

RequestMappingHandlerAdapter 之所以强大,很大程度上是因为它内置了参数解析器返回值处理器两套体系。

参数解析器:HandlerMethodArgumentResolver

Controller 方法的参数五花八门:@RequestParam@PathVariable@RequestBody@ModelAttributeHttpServletRequest......每一种参数都需要从请求中提取并转换成对应的 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 的策略接口和参数解析器类似,也包含 supportsReturnTypehandleReturnValue 两个方法。

五、@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 到响应返回

现在我们把整个流程串起来,看 DispatcherServletdoDispatch 方法是如何一步步处理请求的:

复制代码
① 请求进入 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": "张三"
    }
}

关键点ResponseBodyAdviceHttpMessageConverter 序列化之前介入,可以对返回值做统一的预处理。注意 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 中注册自定义解析器
ResponseBodyAdviceString 类型返回值失效,报类型转换错误 StringHttpMessageConverter 要求返回值必须是 String 类型 beforeBodyWrite 中对 String 类型做特殊处理,返回 Result 的 JSON 字符串

疑难深度追问

Q1:DispatcherServletdoDispatch 方法中,为什么要把"执行拦截器"放在"获取 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 方法。

思考与延伸

  1. 动手验证 :在 doDispatch 的各个关键节点(getHandlerapplyPreHandleha.handleapplyPostHandle)打断点,用 Debug 模式启动一个 Spring Boot 应用,发送一个请求,观察整个调用链的执行顺序。

  2. 思考题HandlerInterceptorpostHandle 在什么情况下不会被执行?(提示:preHandle 返回 false,或 Controller 方法抛出异常且未被 @ExceptionHandler 捕获)

  3. 延伸阅读 :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