核心组件协作
本章是 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 应用时,四个组件按以下时序协作:
上图展示了四个组件的完整协作链路。注意几个关键设计:
- DispatcherServlet 从不直接调用 Controller:它通过 HandlerAdapter 间接调用,这是适配器模式的体现
- HandlerMapping 返回的不是裸 Controller:而是包装了拦截器的 HandlerExecutionChain,这是职责链模式的体现
- 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 就无法统一调用 |
| ViewResolver | Controller 返回逻辑视图名后报 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 四组件的协作流程
标准回答:
- DispatcherServlet 接收 HTTP 请求
- 调用 HandlerMapping 查找匹配的处理器,返回 HandlerExecutionChain(含拦截器链)
- 执行拦截器 preHandle,若全部放行则继续
- 根据 handler 类型获取对应的 HandlerAdapter
- HandlerAdapter 执行参数绑定,调用 Controller 方法
- Controller 返回 ModelAndView(或数据)
- 执行拦截器 postHandle
- 若有视图名,调用 ViewResolver 解析为具体 View 对象
- View 渲染输出
- 执行拦截器 afterCompletion
- 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 路径变量等细节。