乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

核心组件协作

本章是 Spring MVC 教程的机制拆解层。前两章分别回答了"为什么需要 Spring MVC"和"为什么用 DispatcherServlet 作为唯一入口",本章深入 DispatcherServlet 的内部,拆解它如何与 HandlerMapping、HandlerAdapter、ViewResolver 三个核心组件协作,完成从"HTTP 请求"到"HTTP 响应"的完整调度。理解这四个组件的分工与协作,是后续学习拦截器、参数绑定、视图解析等所有细节的结构基础。


定义与作用

Spring MVC 的请求处理不是由 DispatcherServlet 独自完成的,而是四个核心组件各司其职、协同配合的结果。这四个组件构成了 Spring MVC 的"调度骨架":

组件核心职责一句话概括
DispatcherServlet接收所有 HTTP 请求,协调其他组件调度中心:"请求来了,我来安排"
HandlerMapping根据 URL 找到对应的处理器地址查询:"/users/1 该找谁处理?"
HandlerAdapter屏蔽不同处理器的调用差异,统一执行标准化调用:"不管你是谁,我都能调用你"
ViewResolver把逻辑视图名解析为具体视图对象视图解析:"login 对应哪个模板文件?"

这四个组件的关系可以用一个比喻理解:DispatcherServlet 是医院总挂号台,HandlerMapping 是科室分布表,HandlerAdapter 是通用翻译器(让挂号台能跟任何科室的医生沟通),ViewResolver 是报告打印室(把"体检报告"这个逻辑名称对应到具体的打印模板)。


核心原理

四组件协作全景时序图

当一个 HTTP 请求进入 Spring MVC 应用时,四个组件按以下时序协作:

上图展示了四个组件的完整协作链路。注意几个关键设计:

  1. DispatcherServlet 从不直接调用 Controller:它通过 HandlerAdapter 间接调用,这是适配器模式的体现
  2. HandlerMapping 返回的不是裸 Controller:而是包装了拦截器的 HandlerExecutionChain,这是职责链模式的体现
  3. ViewResolver 可以替换:Spring MVC 支持多种 ViewResolver(InternalResourceViewResolver、ThymeleafViewResolver 等),这是策略模式的体现

每个组件对应的设计模式

组件设计模式模式体现
DispatcherServlet前端控制器模式统一接收所有请求,再分发给具体处理器
HandlerAdapter适配器模式将不同类型的处理器(Controller、HttpRequestHandler、Servlet 等)适配为统一的 handle() 接口
ViewResolver策略模式多种视图解析策略可插拔替换(JSP、Thymeleaf、FreeMarker)
HandlerExecutionChain职责链模式拦截器按顺序组成链条,依次执行 preHandle → postHandle → afterCompletion

组件缺失故障分析

如果四个核心组件中缺少任何一个,Spring MVC 的请求处理链路将直接断裂:

缺失组件故障现象根本原因
HandlerMapping所有请求报 404,日志显示 "No handler found"DispatcherServlet 找不到"该找谁处理",无法继续分发
HandlerAdapter找到 Controller 但无法调用,报 "No adapter for handler"Controller 方法签名各异,没有 Adapter 就无法统一调用
ViewResolverController 返回逻辑视图名后报 404,提示 "Could not resolve view"逻辑视图名(如 "login")无法映射到具体模板文件路径
DispatcherServlet应用无法启动,或所有请求由 Servlet 容器直接处理没有调度中心,其他三个组件无法被串联起来

实际案例:小崔曾在项目中手动配置了 @EnableWebMvc,但忘记注册 InternalResourceViewResolver。结果 Controller 正常执行,返回逻辑视图名 "user/list",但客户端始终收到 404。排查三小时后,白歌在白板上画出四组件协作图,一眼指出"ViewResolver 缺失"——逻辑视图名永远解析不成物理路径。

请求流经四组件的详细过程

为了更清晰地理解协作细节,下面按阶段拆解:

阶段一:地址查询(HandlerMapping)

DispatcherServlet 收到请求后,遍历所有已注册的 HandlerMapping 实现(默认有 RequestMappingHandlerMapping、BeanNameUrlHandlerMapping 等)。每个 HandlerMapping 根据自己的规则检查"这个 URL 我能不能处理"。

// DispatcherServlet 内部逻辑(简化)
for (HandlerMapping mapping : this.handlerMappings) {
    HandlerExecutionChain chain = mapping.getHandler(request);
    if (chain != null) {
        return chain; // 找到匹配的处理链
    }
}

阶段二:标准化调用(HandlerAdapter)

找到 HandlerExecutionChain 后,DispatcherServlet 需要调用其中的 handler。但 handler 的类型可能是 Controller 接口、HttpRequestHandler 接口、或者带 @RequestMapping 注解的方法。HandlerAdapter 的作用是:不管 handler 是什么类型,我都能调用它。

// DispatcherServlet 内部逻辑(简化)
for (HandlerAdapter adapter : this.handlerAdapters) {
    if (adapter.supports(handler)) {
        return adapter; // 找到能调用这个 handler 的适配器
    }
}

Spring MVC 默认注册三种 HandlerAdapter:

  • RequestMappingHandlerAdapter:处理 @RequestMapping 注解的方法(最常用)
  • HttpRequestHandlerAdapter:处理 HttpRequestHandler 接口的实现
  • SimpleControllerHandlerAdapter:处理 Controller 接口的实现

阶段三:视图解析(ViewResolver)

Controller 返回 ModelAndView 后,其中的 view 是逻辑名称(如 "user/detail")。ViewResolver 负责把这个逻辑名称翻译成物理资源:

// InternalResourceViewResolver 的工作方式
String viewName = "user/detail";
String url = prefix + viewName + suffix;  
// /WEB-INF/views/ + user/detail + .jsp
// = /WEB-INF/views/user/detail.jsp

完整示例

场景

飞翔科技的员工管理系统进入二期开发,需要增加"部门报表导出"功能。CTO 大翔要求白歌在架构评审会上向团队讲解四组件协作机制,确保小崔、黄俪、李眉都理解各自工作在整个链路中的位置。

白歌的架构讲解

白歌在白板上画了四组件协作图,然后结合代码逐层讲解:

第一步:HandlerMapping 如何找到处理器

@RestController
@RequestMapping("/api/reports")
public class ReportController {

    @GetMapping("/department/{deptId}")
    public ModelAndView exportDepartmentReport(@PathVariable int deptId) {
        // 业务逻辑:查询部门数据
        DepartmentReport report = reportService.generateDeptReport(deptId);
        
        // 返回逻辑视图名 + 数据模型
        ModelAndView mav = new ModelAndView("reports/department");
        mav.addObject("report", report);
        return mav;
    }
}

白歌讲解:"当黄俪的前端发起 GET /api/reports/department/5 时,DispatcherServlet 问 HandlerMapping:'这个 URL 该找谁?' RequestMappingHandlerMapping 检查自己的映射表,发现 @GetMapping("/department/{deptId}") 匹配,于是返回 HandlerExecutionChain,里面包含 ReportController.exportDepartmentReport 方法和挂载的拦截器链。"

第二步:HandlerAdapter 如何统一调用

// 假设小崔写了一个非注解式的老式 Controller
public class LegacyReportController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) {
        // 老式 Controller 接口
        return new ModelAndView("reports/legacy");
    }
}

白歌讲解:"小崔注意,Spring MVC 同时支持注解式 Controller 和老式 Controller 接口。如果没有 HandlerAdapter,DispatcherServlet 就要写一堆 if-else:'如果是注解式,用反射调用;如果是 Controller 接口,调 handleRequest()'。HandlerAdapter 消除了这种混乱——SimpleControllerHandlerAdapter 负责老式接口,RequestMappingHandlerAdapter 负责注解方法,DispatcherServlet 只需要问 '谁能处理这个 handler?',拿到 Adapter 后直接调用 handle(),完全不用关心 handler 的内部类型。"

第三步:ViewResolver 如何解析视图

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}

白歌讲解:"Controller 返回的逻辑视图名是 reports/department。ViewResolver 给它加上前缀 /WEB-INF/views/ 和后缀 .jsp,得到物理路径 /WEB-INF/views/reports/department.jsp。李眉运维时如果想把模板引擎从 JSP 换成 Thymeleaf,只需要把 InternalResourceViewResolver 换成 ThymeleafViewResolver,Controller 代码完全不用改——这就是策略模式的好处。"

第四步:拦截器链如何工作

// 权限检查拦截器
public class ReportAuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
        // 报表功能只有经理以上级别能访问
        User user = (User) req.getSession().getAttribute("user");
        if (!user.hasRole("MANAGER")) {
            resp.setStatus(403);
            return false; // 拦截,不继续执行
        }
        return true;
    }
}

// 耗时统计拦截器
public class PerformanceInterceptor implements HandlerInterceptor {
    private static final Logger log = LoggerFactory.getLogger(PerformanceInterceptor.class);
    private long startTime;
    
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
        startTime = System.currentTimeMillis();
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, 
                                Object handler, Exception ex) {
        long duration = System.currentTimeMillis() - startTime;
        log.info("{} {} 耗时 {}ms", req.getMethod(), req.getRequestURI(), duration);
    }
}

白歌讲解:"HandlerExecutionChain 把这两个拦截器包装成链条。请求到达时,先执行 ReportAuthInterceptor.preHandle——没权限直接 403 返回,有权限继续。然后执行 PerformanceInterceptor.preHandle 记录开始时间。Controller 执行完后,先执行 PerformanceInterceptor 的 postHandle,再执行 ReportAuthInterceptor 的 postHandle。最后视图渲染完成,依次执行两者的 afterCompletion。李眉,你加的监控逻辑就放在 afterCompletion 里,保证无论成功失败都会记录。"

团队反馈:

  • 小崔:"原来 HandlerAdapter 是干这个的。我之前一直疑惑为什么 Controller 方法签名可以各种各样(有的带 Model,有的带 HttpServletRequest,有的返回 String,有的返回 ModelAndView),现在明白了——Adapter 负责把各种签名统一适配成 handle() 调用。"
  • 黄俪:"所以前端调用的 404 可能不是 URL 写错了,也可能是 HandlerMapping 没找到、或者 ViewResolver 没配好?"
  • 白歌:"完全正确。404 的排查要分阶段:先看 HandlerMapping 有没有找到 handler,再看 ViewResolver 能不能解析视图名。"
  • 李眉:"我理解了,拦截器链是在 HandlerExecutionChain 里统一管理的,不是分散在各个 Controller 里。"
  • 大翔:"这个讲解很清晰。小崔,后续写代码时时刻记住自己写的 Controller 只是四组件协作链路中的一环,不要试图在 Controller 里做编码处理、权限检查这些该由前置组件做的事。"

易错场景与面试考点

误区一:HandlerMapping 和 HandlerAdapter 是同一个东西

错误认知:"HandlerMapping 找到 Controller 后,直接调用就行了,为什么还要 HandlerAdapter?"

纠正:HandlerMapping 的职责是**"找对人"(根据 URL 定位处理器),HandlerAdapter 的职责是"能调用"(屏蔽处理器的类型差异)。两者是解耦**的:

  • HandlerMapping 只关心 URL → handler 的映射关系
  • HandlerAdapter 只关心 handler → 调用方式的适配

如果没有这种解耦,每增加一种处理器类型,就要同时修改 HandlerMapping 和 DispatcherServlet 的调用逻辑。有了 HandlerAdapter,新增处理器类型只需新增一个 Adapter 实现,其他组件完全无感知。

误区二:ViewResolver 只在返回 HTML 时有用

错误认知:"我用 @RestController 返回 JSON,ViewResolver 就不参与工作了。"

纠正:@RestController 确实不走 ViewResolver,但 @Controller 返回 String 时默认会被 ViewResolver 解析。更隐蔽的陷阱是:如果 Controller 方法返回 void,Spring MVC 会尝试用请求路径作为逻辑视图名去解析。小崔曾写了一个下载接口返回 void,结果意外触发了 ViewResolver,报了 404。

正确做法:

@Controller
public class FileController {
    
    // 错误:返回 void 会触发 ViewResolver
    @GetMapping("/download")
    public void downloadWrong(HttpServletResponse resp) { ... }
    
    // 正确:返回 ResponseEntity 或标注 @ResponseBody,明确不走视图解析
    @GetMapping("/download")
    @ResponseBody
    public ResponseEntity<byte[]> downloadCorrect() { ... }
}

误区三:拦截器链可以随意调整顺序

错误认知:"拦截器顺序不重要,反正都会执行。"

纠正:拦截器链是职责链模式,preHandle 按顺序执行,但一旦某个拦截器返回 false,后续拦截器和 Controller 都不会执行。而 postHandle 和 afterCompletion 的执行顺序与 preHandle 相反(类似栈结构)。

// 错误配置:权限检查放在日志记录之后
registry.addInterceptor(new LogInterceptor());      // 先记录日志
registry.addInterceptor(new AuthInterceptor());     // 后检查权限
// 结果:未登录用户的请求被记录了日志,然后被 AuthInterceptor 拦截
// 日志里出现大量 401 请求,污染监控数据

// 正确配置:权限检查优先
registry.addInterceptor(new AuthInterceptor());     // 先检查权限
registry.addInterceptor(new LogInterceptor());      // 后记录日志(只记录合法请求)

面试高频:Spring MVC 四组件的协作流程

标准回答:

  1. DispatcherServlet 接收 HTTP 请求
  2. 调用 HandlerMapping 查找匹配的处理器,返回 HandlerExecutionChain(含拦截器链)
  3. 执行拦截器 preHandle,若全部放行则继续
  4. 根据 handler 类型获取对应的 HandlerAdapter
  5. HandlerAdapter 执行参数绑定,调用 Controller 方法
  6. Controller 返回 ModelAndView(或数据)
  7. 执行拦截器 postHandle
  8. 若有视图名,调用 ViewResolver 解析为具体 View 对象
  9. View 渲染输出
  10. 执行拦截器 afterCompletion
  11. DispatcherServlet 返回 HTTP 响应

面试高频:如果 Controller 返回 JSON,ViewResolver 还工作吗?

标准回答:不工作。当使用 @RestController 或方法标注 @ResponseBody 时,Spring MVC 通过 RequestMappingHandlerAdapter 中的 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)直接将返回值序列化为 JSON 写入响应体,跳过 ViewResolver 和 View 渲染阶段。此时四组件协作链路中的"视图解析"和"视图渲染"两个环节被替换为"消息转换"环节。


小结

Spring MVC 的请求处理是DispatcherServlet、HandlerMapping、HandlerAdapter、ViewResolver 四个核心组件协同配合的结果。DispatcherServlet 作为调度中心,通过 HandlerMapping 找到处理器,通过 HandlerAdapter 统一调用,通过 ViewResolver 解析视图。四个组件分别对应前端控制器、职责链、适配器、策略四种经典设计模式,共同构成了一套高内聚、低耦合、可扩展的请求处理框架。

本章与全局的关系:本章拆解了 DispatcherServlet 的内部协作机制。下一章"控制器与请求映射"将聚焦 HandlerMapping 的具体实现——@RequestMapping 如何工作、URL 匹配规则、RESTful 路径变量等细节。

上一页
DispatcherServlet