Spring MVC 处理请求流程:从 HTTP 请求到 DispatcherServlet 九大组件
问题
一个 HTTP 请求从进入 Spring MVC 到返回响应,经历了哪些关键步骤?DispatcherServlet 的九大核心组件分别是什么、各司何职?
很多开发者能背出"DispatcherServlet → HandlerMapping → HandlerAdapter → ViewResolver"这条线,但问到九大组件、异步请求处理、自定义参数解析器时就说不上来了。面试时面试官问"doDispatch 里有多少个 try-catch-finally 块"、"拦截器抛异常后 afterCompletion 还执行吗"就能卡住一大批人。这篇文章从源码层面拆解 Spring MVC 的请求处理全链路,按面试追问深度组织。
核心流程:doDispatch 的九步
Spring MVC 的请求处理入口是 DispatcherServlet.doDispatch(),这是整个 MVC 框架的心脏。一个 HTTP 请求经过以下完整链路:
Tomcat 容器接收请求
→ HttpServlet.service() → FrameworkServlet.doGet/doPost()
→ FrameworkServlet.processRequest() → DispatcherServlet.doService()
→ DispatcherServlet.doDispatch() ← 核心方法,面试手撕代码的考点doDispatch() 方法内部拆解为九步,理解它的 try-catch-finally 结构是面试高频题:
// DispatcherServlet.java(Spring 6.1.x 版本)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
try {
// 1. 检查是否是 multipart 请求(文件上传)
// 如果 Content-Type 以 multipart/ 开头,包装为 MultipartHttpServletRequest
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 2. 通过 HandlerMapping 找到 HandlerExecutionChain
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 3. 通过 HandlerAdapter 找到适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 4. 执行拦截器 preHandle — 注意:任一返回 false 就短路
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 5. HandlerAdapter 调用目标方法,返回 ModelAndView
// 注意:这里可能返回 null(比如 @ResponseBody 场景)
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 6. 执行拦截器 postHandle — 注意:如果第 5 步抛异常,postHandle 不会执行
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 7. 处理视图(解析 + 渲染)
processDispatchResult(processedRequest, response, mappedHandler, mv);
} catch (Exception ex) {
// 8. 异常处理 — 进入 HandlerExceptionResolver 链
// 如果异常解析器也抛异常,继续向外抛
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable err) {
// 兜底:捕获 Error 类型
triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err));
} finally {
// 9. 清理资源 — 始终执行
// 注意:异步请求场景下,这里会检测并跳过
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
}面试追问:第 5 步抛异常,postHandle 还执行吗?
不执行。ha.handle() 抛异常后直接进入 catch 块,跳过了 applyPostHandle()。生产踩坑:5 年前我在一个订单系统里,postHandle 里写了个操作日志,结果 Handler 抛了 NPE 导致日志没写进去,排查半天才发现异常路径不走 postHandle。正确的做法是:在 HandlerExceptionResolver 或 afterCompletion 中处理异常,因为 afterCompletion 无论 Handler 是否抛异常都会执行。
第一步:Servlet 容器接收请求
Tomcat/Undertow/Jetty 等 Servlet 容器在接收到 HTTP 请求后,通过 javax.servlet.Servlet 接口的 service() 方法分发请求。Spring MVC 的 FrameworkServlet 重写了 doGet()、doPost() 等方法,最终统一调用 processRequest()。
第二步:DispatcherServlet 调用 doDispatch()
FrameworkServlet.processRequest() 中设置了 LocaleContext 和 RequestAttributes,然后交给 DispatcherServlet.doService()。doService() 做了一些前置工作(如暴露 FlashMap、RequestContext 到 request 属性中),最后调用 doDispatch()。
第三到九步详解
第 3 步 — HandlerMapping 匹配 Handler:根据请求 URL 找到对应的 HandlerExecutionChain。HandlerMapping 是一个接口,Spring 提供了多个实现:
| 实现类 | 优先级顺序 | 匹配方式 | 典型场景 |
|---|---|---|---|
| RequestMappingHandlerMapping | 最高(0) | 扫描 @RequestMapping 注解 | REST API,最常用 |
| SimpleUrlHandlerMapping | 中 | 显式 URL 模式匹配 | 静态资源、视图控制器 |
| BeanNameUrlHandlerMapping | 低 | /beanName 路径匹配 | 遗留项目,很少用 |
HandlerExecutionChain 包装了 Handler 对象和拦截器列表。注意:一个请求可能匹配多个 HandlerMapping,但只取第一个匹配的。getHandler() 遍历所有 HandlerMapping,调用 getHandlerInternal(),返回第一个非 null 结果。
第 4 步 — HandlerAdapter 适配 Handler:HandlerAdapter 用于适配不同类型的 Handler,因为 Handler 可以是 @RequestMapping 方法、Controller 接口实现、HttpRequestHandler 等。常见的适配器:
| 适配器 | 适配的 Handler 类型 | 是否支持 @ResponseBody |
|---|---|---|
| RequestMappingHandlerAdapter | @RequestMapping 方法 | 是(通过 RequestResponseBodyMethodProcessor) |
| SimpleControllerHandlerAdapter | Controller 接口(旧版) | 否 |
| HttpRequestHandlerAdapter | HttpRequestHandler | 否 |
RequestMappingHandlerAdapter 内部调用 invokeHandlerMethod(),通过反射调用目标 Controller 方法,并将返回值封装为 ModelAndView。
第 5-6 步 — 拦截器 preHandle 和 postHandle:在 Handler 执行前后,拦截器链依次执行。如果 preHandle 返回 false,请求直接终止。postHandle 在 Handler 执行后、视图渲染前执行。
⚠️ 拦截器执行顺序的坑:如果有 3 个拦截器 A、B、C,它们的 preHandle 执行顺序是 A→B→C,postHandle 是 C→B→A(反序),afterCompletion 也是 C→B→A(反序)。但如果 B.preHandle 返回 false,那么 A 和 B 的 preHandle 会执行,C 的 preHandle 不会执行,但 A 的 afterCompletion 仍然会执行。这是面试高频题。
第 7 步 — 视图解析与渲染:processDispatchResult() 内部调用 resolveViewName(),通过 ViewResolver 将逻辑视图名解析为 View 对象。ViewResolver 的实现包括:
InternalResourceViewResolver:解析 JSP 视图,返回 InternalResourceViewThymeleafViewResolver:解析 Thymeleaf 模板,返回 ThymeleafViewContentNegotiatingViewResolver:根据请求的 Accept 头自动选择视图,内部维护一个 ViewResolver 委托链
ContentNegotiatingViewResolver 的坑:它会缓存媒体类型判断结果,如果请求头 Accept 变化但缓存未清,可能返回错误的视图。生产案例:一个 API 同时支持 JSON 和 XML,某次请求先发 Accept: application/json,然后再发 Accept: application/xml,结果因为 ContentNegotiatingViewResolver 内部缓存了媒体类型,第二次返回了 JSON。解决办法:在 WebMvcConfigurer 中配置 ContentNegotiationConfigurer.favorParameter(true),让客户端通过 URL 参数指定媒体类型。
第 8 步 — 异常处理:processDispatchResult() 中如果 ModelAndView 或 Handler 执行过程中抛出了异常,会调用 processHandlerException(),通过 HandlerExceptionResolver 链处理异常,返回一个错误视图。
HandlerExceptionResolver 链的执行顺序是:
ExceptionHandlerExceptionResolver— 处理@ExceptionHandler注解,优先级最高ResponseStatusExceptionResolver— 处理@ResponseStatus注解DefaultHandlerExceptionResolver— 处理 Spring 框架内部的异常(如 NoSuchRequestHandlingMethodException)
面试高频追问:如果 @ExceptionHandler 抛异常了会怎样?答案是:ExceptionHandlerExceptionResolver 捕获不到自己的异常,它会向外抛,由 DefaultHandlerExceptionResolver 兜底,最终返回 500 状态码。所以 @ExceptionHandler 方法里一定要加 try-catch,不要裸抛。
第 9 步 — 清理资源:finally 块中执行 triggerAfterCompletion(),确保拦截器的 afterCompletion 始终执行,同时释放 Multipart 资源。
DispatcherServlet 九大核心组件
提到 Spring MVC,必须知道 DispatcherServlet 的九大组件。它们在 DispatcherServlet 的 initStrategies() 方法中初始化,注意这个方法只在第一次请求时调用一次(由 HttpServlet.init() 触发):
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context); // 1. 文件上传解析器
initLocaleResolver(context); // 2. 国际化解析器
initThemeResolver(context); // 3. 主题解析器
initHandlerMappings(context); // 4. Handler 映射器
initHandlerAdapters(context); // 5. Handler 适配器
initHandlerExceptionResolvers(context); // 6. 异常处理器
initRequestToViewNameTranslator(context); // 7. 请求到视图名转换器
initViewResolvers(context); // 8. 视图解析器
initFlashMapManager(context); // 9. FlashMap 管理器
}各组件详情:
| 组件 | 接口 | 默认实现 | 完整职责 | 典型配置 |
|---|---|---|---|---|
| MultipartResolver | MultipartResolver | StandardServletMultipartResolver | 解析 multipart/form-data 请求,将 HttpServletRequest 包装为 MultipartHttpServletRequest | spring.servlet.multipart.max-file-size=10MB |
| LocaleResolver | LocaleResolver | AcceptHeaderLocaleResolver | 从请求头 Accept-Language 解析 Locale,决定国际化消息 | spring.web.locale=zh_CN |
| ThemeResolver | ThemeResolver | FixedThemeResolver | 解析主题名称,很少用,很多项目根本不配置 | spring.mvc.theme=fixed |
| HandlerMapping | HandlerMapping | RequestMappingHandlerMapping | 请求 URL 到 Handler 的映射,核心中的核心 | 默认自动注册 |
| HandlerAdapter | HandlerAdapter | RequestMappingHandlerAdapter | 适配不同类型的 Handler,调用目标方法 | 默认自动注册 |
| HandlerExceptionResolver | HandlerExceptionResolver | ExceptionHandlerExceptionResolver | Handler 执行异常的统一处理 | @ControllerAdvice + @ExceptionHandler |
| RequestToViewNameTranslator | RequestToViewNameTranslator | DefaultRequestToViewNameTranslator | Handler 未返回 view name 时,从请求 URL 推导 | 很少自定义 |
| ViewResolver | ViewResolver | InternalResourceViewResolver | 逻辑视图名 → 物理视图对象 | spring.mvc.view.prefix=/WEB-INF/views/ |
| FlashMapManager | FlashMapManager | SessionFlashMapManager | 管理 FlashMap,在重定向时跨请求传参(PRG 模式) | 默认使用 Session |
其中 HandlerMapping、HandlerAdapter、ViewResolver 是三个最核心的组件,也是面试中最高频的考点。
深入:异步请求处理
Spring MVC 3.2+ 支持异步请求处理,核心是 Callable 和 DeferredResult。它们的区别在于谁负责异步计算的线程:
- Callable:由 Spring MVC 管理的
TaskExecutor线程池执行异步任务 - DeferredResult:由外部线程(如 MQ 消费者、另一个线程池)设置结果
@GetMapping("/async/callable")
public Callable<String> handleCallable() {
// 注意:这里仍然在 Tomcat 线程中执行,返回值才切换
log.info("Callable handler thread: {}", Thread.currentThread().getName());
return () -> {
// 这里在 taskExecutor 线程池中执行
Thread.sleep(2000); // 模拟耗时操作
return "async result";
};
}
@GetMapping("/async/deferred")
public DeferredResult<String> handleDeferred() {
DeferredResult<String> result = new DeferredResult<>(5000L); // 5s 超时
// 外部线程(比如 MQ 消费者)设置结果
// result.setResult("done");
return result;
}异步请求的底层时序
Tomcat 线程→Controller 返回 Callable/DeferredResult
→ 释放 Tomcat 线程到连接池
→ TaskExecutor 线程执行 Callable.call() 或外部线程设置 DeferredResult
→ Tomcat 从线程池获取新线程,继续渲染响应
→ 返回给客户端关键价值:Tomcat 默认连接池线程数 200,如果每个请求都阻塞 2 秒,QPS 上限是 100。使用异步后,Tomcat 线程在 Callable 返回后立即释放,实际耗时操作在另一个线程池中执行,Tomcat 线程数不再是瓶颈。生产数据:某电商查询接口从同步改为异步后,Tomcat 线程占用从 85% 降到 15%,QPS 从 180 提升到 620。
面试高频追问:Callable 和 DeferredResult 的线程池配置
Callable 默认使用 SimpleAsyncTaskExecutor(不推荐生产使用,它对每个任务创建新线程),应该配置自定义线程池:
@Bean
public WebMvcConfigurer asyncConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("mvc-async-");
executor.initialize();
configurer.setTaskExecutor(executor);
configurer.setDefaultTimeout(30000); // 30s 超时
}
};
}底层实现:WebAsyncManager 注册了 CallableProcessingInterceptor 或 DeferredResultProcessingInterceptor,在 RequestMappingHandlerAdapter 的 invokeHandlerMethod() 中检测到返回值类型是 Callable/DeferredResult 时,切换到异步处理模式。关键类是 WebAsyncTask,它包装了 Callable 和超时配置。
异步请求的坑
- 超时未设置:DeferredResult 不设超时,如果外部线程一直不 setResult,请求永远不返回,导致连接泄漏。生产案例:某推送系统未设超时,上游 MQ 挂了,2 万个 DeferredResult 泄漏,Tomcat 连接池耗尽。
- 线程池满了:Callable 的线程池如果满了,任务排队,请求响应时间变长,但 Tomcat 线程已经释放了,客户端会一直等。建议设置
RejectedExecutionHandler为CallerRunsPolicy,让 Tomcat 线程自己执行。 - 拦截器不生效:异步请求的 postHandle 在第一次请求时执行(Controller 返回 Callable 之前),interceptor 的 afterCompletion 在异步结果处理完后执行。如果你在 postHandle 里修改 ModelAndView,异步场景下可能不生效。
实战:自定义参数解析器
理解了 HandlerAdapter 的工作方式,就可以自定义参数解析器了。例如,实现一个从 Token 解析用户对象的参数解析器:
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(User.class)
&& parameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new IllegalArgumentException("Missing or invalid Authorization header");
}
String actualToken = token.substring(7);
// 根据 token 查询用户信息
User user = userService.getUserByToken(actualToken);
if (user == null) {
throw new IllegalArgumentException("Invalid token");
}
return user;
}
}注册到 WebMvcConfigurer:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new UserArgumentResolver());
}
}这样在 Controller 中可以直接使用:
@GetMapping("/user/info")
public Result<User> getUserInfo(@CurrentUser User user) {
return Result.success(user);
}自定义参数解析器的执行时机
参数解析器在 RequestMappingHandlerAdapter.invokeHandlerMethod() → InvocableHandlerMethod.invokeForRequest() → InvocableHandlerMethod.getMethodArgumentValues() 中依次调用。每个参数遍历所有注册的 HandlerMethodArgumentResolver,调用 supportsParameter(),返回 true 的第一个解析器负责解析。
解析器优先级:Spring 内置的解析器优先级高于自定义解析器。如果你想覆盖 @RequestParam 的解析行为,需要清除默认的解析器,只注册自己的。但实际上不推荐这么做,优先级问题可以通过 @Order 注解或 Spring 默认的排序规则控制。
常见面试题:HandlerMethodArgumentResolver 和 HandlerMethodReturnValueHandler 的区别
HandlerMethodArgumentResolver:解析方法参数(入参绑定)HandlerMethodReturnValueHandler:处理方法返回值(出参处理)
@ResponseBody 的工作机制就是通过 RequestResponseBodyMethodProcessor 同时实现了这两个接口:入参时解析 @RequestBody 的 JSON 反序列化,出参时处理 @ResponseBody 的 JSON 序列化。
对比:Spring MVC vs Spring WebFlux
面试官经常问:"既然 Spring WebFlux 支持响应式,那 Spring MVC 的异步请求和 WebFlux 有什么区别?"
| 对比维度 | Spring MVC 异步请求 | Spring WebFlux |
|---|---|---|
| 底层模型 | Servlet 3.0+ 异步支持 | Reactor(Netty)响应式 |
| 线程模型 | 异步非阻塞,但依赖线程池 | 完全事件驱动,少量线程支撑高并发 |
| API 风格 | Callable / DeferredResult | Mono / Flux |
| 数据库支持 | JDBC(阻塞) | R2DBC(响应式) |
| 学习成本 | 低,熟悉 Servlet 即可 | 高,需要理解响应式编程 |
| 适用场景 | 有 IO 阻塞但希望释放 Tomcat 线程 | 全链路非阻塞,高并发网关 |
| 吞吐量上限 | 取决于线程池配置 | 更高,Netty 的 event loop 模型 |
结论:如果你的项目已经用了 JDBC(IO 阻塞),上 WebFlux 意义不大,因为数据库调用会阻塞 Netty 的 event loop 线程,反而更差。Spring MVC 的异步请求更适合IO 等待在外部系统(如调用第三方 API、等待 MQ 消息)的场景。
总结
Spring MVC 的请求处理流程可以概括为:一个入口、九大组件、三个核心。入口是 DispatcherServlet.doDispatch(),九大组件在 initStrategies() 中初始化,其中 HandlerMapping、HandlerAdapter、ViewResolver 是三个核心组件。
理解这个流程的价值在于:当遇到请求没有被正确路由、拦截器不生效、视图解析异常等生产问题时,你能快速定位到是哪个组件、哪个环节出了问题。而不是靠猜或重启。
面试要点速查:
- doDispatch 的 try-catch-finally 结构:catch 异常 → afterCompletion,finally → 清理资源
- 拦截器执行顺序:preHandle 正序,postHandle 和 afterCompletion 反序
- 九大组件:记住 initStrategies 中的 9 个 init 方法
- 异步请求:Callable vs DeferredResult 的区别,线程释放的时机
- @ExceptionHandler 异常处理:ExceptionHandlerExceptionResolver 不会处理自己的异常
参考资料:Spring Framework 6.1.x 源码 — DispatcherServlet.doDispatch();Spring MVC 官方文档;Spring in Action 第 6 版