Skip to content

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 结构是面试高频题:

java
// 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)
SimpleControllerHandlerAdapterController 接口(旧版)
HttpRequestHandlerAdapterHttpRequestHandler

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 视图,返回 InternalResourceView
  • ThymeleafViewResolver:解析 Thymeleaf 模板,返回 ThymeleafView
  • ContentNegotiatingViewResolver:根据请求的 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 链的执行顺序是:

  1. ExceptionHandlerExceptionResolver — 处理 @ExceptionHandler 注解,优先级最高
  2. ResponseStatusExceptionResolver — 处理 @ResponseStatus 注解
  3. DefaultHandlerExceptionResolver — 处理 Spring 框架内部的异常(如 NoSuchRequestHandlingMethodException)

面试高频追问:如果 @ExceptionHandler 抛异常了会怎样?答案是:ExceptionHandlerExceptionResolver 捕获不到自己的异常,它会向外抛,由 DefaultHandlerExceptionResolver 兜底,最终返回 500 状态码。所以 @ExceptionHandler 方法里一定要加 try-catch,不要裸抛。

第 9 步 — 清理资源finally 块中执行 triggerAfterCompletion(),确保拦截器的 afterCompletion 始终执行,同时释放 Multipart 资源。

DispatcherServlet 九大核心组件

提到 Spring MVC,必须知道 DispatcherServlet 的九大组件。它们在 DispatcherServletinitStrategies() 方法中初始化,注意这个方法只在第一次请求时调用一次(由 HttpServlet.init() 触发):

java
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 管理器
}

各组件详情:

组件接口默认实现完整职责典型配置
MultipartResolverMultipartResolverStandardServletMultipartResolver解析 multipart/form-data 请求,将 HttpServletRequest 包装为 MultipartHttpServletRequestspring.servlet.multipart.max-file-size=10MB
LocaleResolverLocaleResolverAcceptHeaderLocaleResolver从请求头 Accept-Language 解析 Locale,决定国际化消息spring.web.locale=zh_CN
ThemeResolverThemeResolverFixedThemeResolver解析主题名称,很少用,很多项目根本不配置spring.mvc.theme=fixed
HandlerMappingHandlerMappingRequestMappingHandlerMapping请求 URL 到 Handler 的映射,核心中的核心默认自动注册
HandlerAdapterHandlerAdapterRequestMappingHandlerAdapter适配不同类型的 Handler,调用目标方法默认自动注册
HandlerExceptionResolverHandlerExceptionResolverExceptionHandlerExceptionResolverHandler 执行异常的统一处理@ControllerAdvice + @ExceptionHandler
RequestToViewNameTranslatorRequestToViewNameTranslatorDefaultRequestToViewNameTranslatorHandler 未返回 view name 时,从请求 URL 推导很少自定义
ViewResolverViewResolverInternalResourceViewResolver逻辑视图名 → 物理视图对象spring.mvc.view.prefix=/WEB-INF/views/
FlashMapManagerFlashMapManagerSessionFlashMapManager管理 FlashMap,在重定向时跨请求传参(PRG 模式)默认使用 Session

其中 HandlerMapping、HandlerAdapter、ViewResolver 是三个最核心的组件,也是面试中最高频的考点。

深入:异步请求处理

Spring MVC 3.2+ 支持异步请求处理,核心是 CallableDeferredResult。它们的区别在于谁负责异步计算的线程

  • Callable:由 Spring MVC 管理的 TaskExecutor 线程池执行异步任务
  • DeferredResult:由外部线程(如 MQ 消费者、另一个线程池)设置结果
java
@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(不推荐生产使用,它对每个任务创建新线程),应该配置自定义线程池:

java
@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 注册了 CallableProcessingInterceptorDeferredResultProcessingInterceptor,在 RequestMappingHandlerAdapterinvokeHandlerMethod() 中检测到返回值类型是 Callable/DeferredResult 时,切换到异步处理模式。关键类是 WebAsyncTask,它包装了 Callable 和超时配置。

异步请求的坑

  1. 超时未设置:DeferredResult 不设超时,如果外部线程一直不 setResult,请求永远不返回,导致连接泄漏。生产案例:某推送系统未设超时,上游 MQ 挂了,2 万个 DeferredResult 泄漏,Tomcat 连接池耗尽。
  2. 线程池满了:Callable 的线程池如果满了,任务排队,请求响应时间变长,但 Tomcat 线程已经释放了,客户端会一直等。建议设置 RejectedExecutionHandlerCallerRunsPolicy,让 Tomcat 线程自己执行。
  3. 拦截器不生效:异步请求的 postHandle 在第一次请求时执行(Controller 返回 Callable 之前),interceptor 的 afterCompletion 在异步结果处理完后执行。如果你在 postHandle 里修改 ModelAndView,异步场景下可能不生效。

实战:自定义参数解析器

理解了 HandlerAdapter 的工作方式,就可以自定义参数解析器了。例如,实现一个从 Token 解析用户对象的参数解析器:

java
@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:

java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new UserArgumentResolver());
    }
}

这样在 Controller 中可以直接使用:

java
@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 / DeferredResultMono / 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 版

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。