引言:请求进来之后,到底经过了谁的手
排查 Spring MVC 的问题时,几乎所有报障都能归到"请求流程"上的某一环:接口返回 404,是没有任何 Handler 认领这个 URL;接口返回 406/415,是找不到合适的消息转换器;页面渲染报"找不到视图",是 ViewResolver 解析失败;参数怎么传都绑定不上,是参数解析器没有覆盖这种入参形式。这些问题的共同答案,都藏在 DispatcherServlet.doDispatch() 这一个方法里。这篇文章把这条主干流程完整讲清楚:请求怎么被路由到 Controller、方法怎么被找到并调用、返回值怎么变成最终的 HTTP 响应。
一、DispatcherServlet:前端控制器模式的具体实现

Spring MVC 遵循经典的**前端控制器(Front Controller)**模式:所有进入应用的请求先统一交给一个 Servlet(DispatcherServlet),再由它分发给具体的处理逻辑,而不是让每个 URL 对应一个独立的 Servlet。容器启动时的配置(Servlet 容器场景下通常是 <servlet-mapping> 的 url-pattern 设为 /,Spring Boot 场景下由 DispatcherServletAutoConfiguration 自动注册)决定了几乎所有请求都会先经过 DispatcherServlet。
DispatcherServlet 内部持有九个核心策略组件,初始化阶段(initStrategies())会依次加载:
| 组件 | 职责 |
|---|---|
HandlerMapping |
根据请求找到应该由哪个方法处理 |
HandlerAdapter |
用统一的方式去调用形态各异的 Handler |
HandlerExceptionResolver |
统一处理 Handler 执行过程中抛出的异常 |
ViewResolver |
把逻辑视图名解析成真正的 View 对象 |
RequestToViewNameTranslator |
Handler 没有显式返回视图名时,按约定推断一个 |
LocaleResolver / ThemeResolver |
国际化与主题相关的辅助解析 |
MultipartResolver |
识别并解析文件上传请求 |
FlashMapManager |
管理重定向场景下跨请求传递的临时数据 |
这篇文章聚焦其中最核心的三个:HandlerMapping(找谁处理)、HandlerAdapter(怎么调用)、ViewResolver(怎么渲染),它们串联起了一次请求从进入到响应的完整生命周期。

二、HandlerMapping:请求怎么找到对应的方法

启动阶段:建立 URL 到方法的映射表
容器启动时,RequestMappingHandlerMapping(最常用的 HandlerMapping 实现)会扫描所有 @Controller/@RequestMapping 标注的 Bean,把类级别路径和方法级别路径拼接后,注册进一张内部映射表(MappingRegistry),核心结构大致是:
// 简化表示,真实实现按 RequestMappingInfo 做更精细的匹配(HTTP方法、参数、头部条件等)
Map<RequestMappingInfo, HandlerMethod> registry;
RequestMappingInfo 不只包含 URL 路径,还包含 HTTP 方法(GET/POST 等)、请求参数条件、请求头条件、内容类型条件——这也是为什么同一个路径可以用 @GetMapping 和 @PostMapping 分别映射到两个不同方法,它们在注册表里是两条独立的记录。

请求到达时: 的匹配过程
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
return null;
}
DispatcherServlet 会依次询问容器里所有的 HandlerMapping(不止 RequestMappingHandlerMapping,静态资源也有专门的 HandlerMapping),第一个返回非空结果的就被采用。返回的不是一个裸的方法引用,而是一个 HandlerExecutionChain——它把目标 HandlerMethod 和这次请求需要经过的一串 HandlerInterceptor 打包在一起。
排查 404 的三个常见根源
- 路径确实没有任何 Controller 声明——最直接的情况,检查类级
@RequestMapping和方法级路径拼接后的最终路径。 - 静态资源映射抢先命中。Spring Boot 默认会给
/static、/public等目录下的资源注册一个SimpleUrlHandlerMapping,如果业务路径恰好和静态资源路径规则冲突,可能被错误的HandlerMapping提前"认领"(返回 404 或者返回静态文件本身,具体取决于文件是否存在)。 - HTTP 方法不匹配:路径本身注册了,但只注册了
@PostMapping,用 GET 请求访问会匹配不上RequestMappingInfo里的方法条件,这种情况实际会得到 405 而不是 404——这本身就是HandlerMapping匹配逻辑区分"路径存在但条件不满足"和"路径完全不存在"这两种情形的直接体现。
三、HandlerAdapter:用适配器模式抹平 Handler 形态的差异
为什么需要一层适配器
Spring MVC 历史上支持过多种风格的 Handler:实现 Controller 接口的类、实现 HttpRequestHandler 接口的类,以及现在最常用的、用 @RequestMapping 系列注解标注的普通方法(HandlerMethod)。这些 Handler 的方法签名完全不同,DispatcherServlet 不可能对每种形态写一段专门的调用代码,于是引入了适配器模式:HandlerAdapter 接口统一暴露一个 handle(request, response, handler) 方法,具体怎么调用目标 Handler 由各自的适配器实现类内部处理。
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}
DispatcherServlet 找到 Handler 之后,同样是遍历容器里所有的 HandlerAdapter,调用 supports() 找到第一个能处理当前 Handler 类型的适配器——这跟 HandlerMapping 的"遍历 + 第一个命中"是同一套设计思路。

:现代 Spring MVC 的核心适配器
对于最常见的 @RequestMapping 方法,真正干活的是 RequestMappingHandlerAdapter.invokeHandlerMethod()。这里有两组关键组件:
**HandlerMethodArgumentResolver**:负责把HttpServletRequest里的各种数据(路径变量、查询参数、请求体、请求头、Cookie……)解析并绑定成目标方法的入参。每一种参数类型(@PathVariable、@RequestParam、@RequestBody、HttpServletRequest本身等)都对应一个具体的解析器实现,方法调用前会遍历所有已注册的解析器,找到第一个supportsParameter()返回true的来处理这个参数。**HandlerMethodReturnValueHandler**:方法执行完之后,返回值同样需要被"翻译"成响应——返回String被当作视图名,返回ModelAndView直接使用,标注了@ResponseBody(或类上标了@RestController)的返回值会被交给HttpMessageConverter直接序列化写入响应体,完全不走视图解析这条路。
这组"参数解析器 + 返回值处理器"的设计,本质上是把"HTTP 请求/响应"和"普通 Java 方法调用"之间的转换逻辑,拆成了一组可插拔、可扩展的策略对象——这也是为什么你可以自己实现一个 HandlerMethodArgumentResolver 来支持自定义参数注解,完全不需要改动框架源码。

四、:一次请求的完整调用时序
把 HandlerMapping 和 HandlerAdapter 串起来,DispatcherServlet.doDispatch() 的主干逻辑大致是:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerExecutionChain mappedHandler = getHandler(request);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 拦截器链:前置处理,任意一个返回 false 就中断请求
if (!mappedHandler.applyPreHandle(request, response)) {
return;
}
ModelAndView mv = null;
try {
mv = ha.handle(request, response, mappedHandler.getHandler());
}
catch (Exception ex) {
// 异常会被 processHandlerException 交给 HandlerExceptionResolver 链条处理
// 处理成功则得到一个"异常对应的错误视图"ModelAndView,继续走正常渲染流程
}
// 拦截器链:后置处理,能拿到 ModelAndView 并可以修改
mappedHandler.applyPostHandle(request, response, mv);
// 渲染视图 / 如果已经通过 @ResponseBody 写完响应,这一步基本是空操作
processDispatchResult(request, response, mappedHandler, mv, null);
// 不管成功还是失败,最终触发拦截器的 afterCompletion
mappedHandler.triggerAfterCompletion(request, response, null);
}
这条主干逻辑解释了拦截器(HandlerInterceptor)三个方法各自的执行时机:preHandle 在真正调用 Handler 之前、可以直接中断请求;postHandle 在 Handler 执行完之后、视图渲染之前,可以修改 ModelAndView;afterCompletion 无论成功还是异常都会执行,适合做资源清理、日志记录。这也是为什么"权限校验"通常放在 preHandle,而"记录响应耗时"通常放在 afterCompletion——这不是约定俗成的最佳实践,而是这条调用时序本身决定的。

五、ViewResolver:逻辑视图名怎么变成真实响应

返回视图名这条路径
如果 Handler 方法返回的是一个 String(视图名)或者 ModelAndView,processDispatchResult 最终会调用 resolveViewName():
protected View resolveViewName(String viewName, Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
同样是"遍历所有 ViewResolver,第一个返回非空结果的生效"。得到 View 对象之后,调用 view.render(model, request, response),由具体的视图技术(JSP、Thymeleaf、FreeMarker 等)完成实际的 HTML 渲染并写入响应流。
这条路径:完全绕开 ViewResolver
这是最容易被误解的一点:标注了 @ResponseBody(或 @RestController)的方法,返回值处理在 HandlerMethodReturnValueHandler 阶段就已经完成——对应的处理器(RequestResponseBodyMethodProcessor)直接调用 HttpMessageConverter 把返回对象序列化(比如转成 JSON)并写入响应体,invokeHandlerMethod() 返回的 ModelAndView 是 null。processDispatchResult 拿到 null 的 ModelAndView,不会做任何视图解析,直接跳过。
也就是说,@ResponseBody 和"返回视图名"这两种写法,实际上是两条完全独立的响应生成路径,只是共享了同一个 doDispatch() 主干和同一层拦截器链——理解了这一点,"为什么 REST 接口不需要配置 ViewResolver"这种问题也就有了确切答案:不是不需要配置就能生效,而是这条代码路径压根不会走到 ViewResolver 那一步。

404/415/406 分别对应哪个环节没找到匹配
- 404 Not Found:
HandlerMapping没有找到任何匹配的 Handler。 - 405 Method Not Allowed:路径匹配上了,但 HTTP 方法不在
RequestMappingInfo允许的范围内。 - 415 Unsupported Media Type:请求体的
Content-Type没有任何HttpMessageConverter能处理(比如声明只接受application/json却传了application/xml)。 - 406 Not Acceptable:
Accept请求头指定的响应格式,没有任何HttpMessageConverter能生成(常见于误配了produces属性)。 - 视图渲染报"找不到视图":路径正确、方法执行成功,但
ViewResolver按约定规则(前缀+视图名+后缀)拼出的物理路径下没有对应的模板文件。
把这五种报错对照到具体环节,排查思路就不再是"猜配置哪里错了",而是先判断这次请求卡在流程图上的哪一个节点。

六、小结:从请求进来到响应出去的完整路径
请求到达 DispatcherServlet.doDispatch()
→ getHandler():遍历 HandlerMapping,找到 HandlerExecutionChain(HandlerMethod + 拦截器链)
→ getHandlerAdapter():遍历 HandlerAdapter,找到能处理这个 Handler 类型的适配器
→ mappedHandler.applyPreHandle():拦截器前置处理,可直接中断请求
→ ha.handle():
→ 参数解析:HandlerMethodArgumentResolver 把请求数据绑定成方法入参
→ 执行目标方法
→ 返回值处理:HandlerMethodReturnValueHandler
→ 返回视图名/ModelAndView:走视图解析路径
→ @ResponseBody:HttpMessageConverter 直接写响应体,ModelAndView 为 null
→ mappedHandler.applyPostHandle():拦截器后置处理
→ processDispatchResult():
→ ModelAndView 非空:resolveViewName() 遍历 ViewResolver,得到 View 后 render()
→ ModelAndView 为空:跳过视图渲染(响应已经在 handle 阶段写完)
→ mappedHandler.triggerAfterCompletion():无论成功失败都执行
这张图上的每一个节点,都能对应到一类具体的报障现象。下次遇到"接口莫名其妙不工作",先定位卡在哪个节点,比直接猜配置效率高得多。
文末答疑:面试官会怎么问
Q1:如果同一个应用里注册了两个**HandlerMapping**,一个是自定义的、专门处理某类特殊路径,另一个是 Spring MVC 默认的**RequestMappingHandlerMapping**,两者对同一个路径都能返回非空的匹配结果,最终会用哪一个?这个顺序由什么决定,你会怎么控制它?
A:DispatcherServlet 遍历 handlerMappings 列表时是按顺序取第一个非空结果,这个顺序由各个 HandlerMapping 实现的 Ordered 优先级(getOrder())决定,数值越小优先级越高、越先被询问。如果两个 HandlerMapping 都能匹配同一个路径且没有显式设置顺序,实际生效的是哪一个会依赖它们在容器里的默认注册顺序,这是一种容易踩雷的隐式行为。工程上应该给自定义的 HandlerMapping 显式实现 Ordered 接口或使用 @Order 注解,明确声明它应该在 RequestMappingHandlerMapping 之前还是之后被询问,而不是依赖框架的默认加载顺序。
Q2:**HandlerInterceptor.postHandle()**里能不能修改 Controller 已经准备好的响应数据?如果 Controller 方法标注了**@ResponseBody**,这个修改还生效吗?为什么?
A:如果是返回视图名/ModelAndView 的传统流程,postHandle 拿到的 ModelAndView 还没有被渲染,此时修改其中的 Model 数据是有效的,会体现在最终渲染出的视图里。但如果方法标注了 @ResponseBody,返回值处理阶段已经通过 HttpMessageConverter 把数据序列化并直接写入了响应流,invokeHandlerMethod() 返回的 ModelAndView 是 null——postHandle 这时候拿到的是空对象,任何修改都不会有任何效果,因为响应体已经写完了。这也是为什么如果需要在 @ResponseBody 场景下统一修改返回内容(比如包一层统一响应结构),应该用 ResponseBodyAdvice 这种在消息转换器写入之前介入的扩展点,而不是指望在拦截器的 postHandle 阶段修改。
Q3:假设一个 Spring MVC 应用同时配置了全局异常处理(**@ControllerAdvice**+**@ExceptionHandler**)和自定义的**HandlerExceptionResolver**,Controller 方法抛出异常之后,实际的处理顺序和优先级是怎样的?
A:@ExceptionHandler 的底层实现本身就是一种 HandlerExceptionResolver(ExceptionHandlerExceptionResolver),Spring MVC 在 HandlerExceptionResolverComposite 里维护了一个有序列表,默认包含 ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver 等,自定义的 HandlerExceptionResolver 也会加入这个列表,顺序同样受 Ordered/@Order 控制。异常处理链条会按顺序依次询问每个 Resolver,第一个能处理(返回非 null 的 ModelAndView)的生效,后面的不再执行。所以回答这个问题的关键点是:不存在"全局异常处理器和自定义 Resolver 谁天生优先"的说法,谁先谁后完全取决于它们在这个有序列表里的相对位置,需要显式检查或配置 @Order。
Q4:如果要在不修改任何业务 Controller 代码的前提下,给某一类接口统一增加"请求耗时超过阈值就打警告日志"的能力,你会选择在**HandlerInterceptor**、AOP 切面还是 Filter 里实现?为什么,这个选择跟本文讲的请求流程有什么关系?
A:三者都能拿到"进入"和"退出"两个时间点,但作用范围和能获取的信息不同:Filter 工作在 Servlet 容器层面,比 DispatcherServlet 更早介入,拿不到具体匹配到的是哪个 HandlerMethod,适合做非常通用、跟具体业务路由无关的处理(比如统一编码设置);HandlerInterceptor 工作在 DispatcherServlet 内部,preHandle/afterCompletion 之间正好完整覆盖了 HandlerAdapter.handle() 执行的全过程(包括参数解析和返回值处理),并且能从 HandlerMethod 上拿到方法、注解等元信息,天然适合按 Handler 粒度做统一的耗时统计或日志;AOP 切面则是绑定在具体 Bean 的方法调用上,如果目标是"所有走 MVC 请求流程的接口",用 HandlerInterceptor 更贴合语义,因为它本来就是 Spring MVC 请求流程里专门为这类横切需求设计的扩展点,不需要额外操心"这个方法有没有被 AOP 代理""这次调用是不是从外部发起的"这些本文反复出现过的坑。