第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 中的 "总指挥"角色
  • 掌握 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 的名称映射为 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、@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 方法。

思考与延伸

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

  2. 思考题 :HandlerInterceptor 的 postHandle 在什么情况下不会被执行?(提示: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