Skip to content

SpringMVC 请求全链路:DispatcherServlet 到响应

本文是 Spring 系统学习系列的 L2 核心篇。前置:30. bean-lifecycle-extension-points。 学完可以配合面试题食用:12. DispatcherServlet 处理请求13. 拦截器 Filter AOP14. 全局异常处理

从 Servlet 说起:为什么需要 DispatcherServlet

写原生 Servlet 的年代,一个 URL 对应一个 Servlet 类,参数解析、类型转换、视图跳转全靠手写。SpringMVC 的做法是把这套流程抽成一个"总入口":所有请求先进 DispatcherServlet,由它统一分发给对应的处理器。你写的 @RestController 只负责业务本身,路由、参数绑定、返回值转换这些杂活都在入口层解决了。

继承链值得看一眼:

text
HttpServlet
  └─ FrameworkServlet          // 把 service() 收敛到 doService()
      └─ DispatcherServlet     // doService() -> doDispatch(),核心分发逻辑

DispatcherServlet 本身还是一个 Servlet,没有魔法。它做的事可以类比公司前台:访客(请求)先到前台,前台查通讯录(HandlerMapping)找到对应员工(Handler),把资料递过去,员工处理完的结果再由前台按对方习惯的格式(JSON/页面)回复。

doDispatch 九步流程

doDispatch() 是整个框架的心脏,一次请求大致走这几步:

  1. DispatcherServlet 接到请求,先调 getHandler() 遍历所有 HandlerMapping
  2. HandlerMapping 按 URL + 请求方法匹配到 HandlerExecutionChain(处理器 + 一组拦截器)
  3. 依次执行拦截器的 preHandle(),任一个返回 false 就走异步或直接返回
  4. HandlerAdapter 包装 Handler 并调用——你的 Controller 方法在这里被执行
  5. 返回值交给 HandlerMethodReturnValueHandler 处理
  6. @ResponseBody 场景下走 HttpMessageConverter 把对象写成 JSON
  7. 拦截器 postHandle()
  8. 渲染视图或直接写响应
  9. 拦截器 afterCompletion(),finally 里执行,异常也会走
mermaid
sequenceDiagram
    participant C as Client
    participant D as DispatcherServlet
    participant H as HandlerMapping
    participant I as 拦截器链
    participant A as HandlerAdapter
    participant M as HttpMessageConverter

    C->>D: HTTP 请求
    D->>H: getHandler()
    H-->>D: HandlerExecutionChain
    D->>I: preHandle()
    I-->>D: true
    D->>A: handle()
    A->>A: 反射调用 Controller 方法
    A-->>D: 返回值 + ModelAndView
    D->>M: writeWithMessageConverters()
    M-->>C: JSON 响应
    D->>I: postHandle() / afterCompletion()

有一个细节容易被忽略:HandlerMapping 是列表遍历,不是字典精确查找。常见的 RequestMappingHandlerMapping 只是其中一项,前面还有 BeanNameUrlHandlerMapping 等更老的映射方式。这解释了为什么老项目里一个 URL 不会被两套映射同时接住——先匹配先得,匹配到就短路返回。

@RestController 的返回值是怎么变 JSON 的

Controller 方法返回一个 POJO,浏览器收到的是 JSON 字符串,中间的转换发生在 HandlerAdapter 内部。RequestMappingHandlerAdapter 调用完方法后,按返回值类型选一个 HandlerMethodReturnValueHandler;方法标了 @ResponseBody@RestController 等价于类级 @ResponseBody),就走 RequestResponseBodyMethodProcessor

这个 Processor 做的事叫内容协商:先看请求头 Accept,再看响应默认的 Content-Type,从 HttpMessageConverter 列表里挑出第一个既能写 Java 对象又能产出版本兼容 MIME 类型的转换器。classpath 里有 Jackson 就是 MappingJackson2HttpMessageConverter,有 Fastjson 就换对应的。所以"返回 JSON"不是 Spring 的硬编码,是 classpath 里恰好有 Jackson 这个默认选项。想在同一个接口上支持 Accept: application/xml,加一个 jackson-dataformat-xml 依赖就行,代码一行不改。

拦截器 vs Filter vs AOP

三个都能在请求前后插逻辑,选型看切面位置:

text
请求 -> Filter -> DispatcherServlet -> 拿到 Handler 前后(拦截器) -> Controller 方法调用前后(AOP)
  • Filter 是 Servlet 规范,在 DispatcherServlet 之前。拿不到 Handler 是哪个方法,但能改请求/响应流。字符编码、CORS、XSS 过滤适合放这。
  • 拦截器 是 SpringMVC 概念,知道目标 Handler,能拿到 HandlerMethod 上的注解。登录校验、接口权限、打点耗时适合放这。
  • AOP 作用在 Bean 方法级,不限于 Controller。事务、日志、监控埋点适合放这。

顺序上,preHandle 先注册先执行,postHandle/afterCompletion 反过来。和 Filter 的关系是:FilterChain 包在最外层,preHandle 在所有 Filter 之后。异常场景下 postHandle 会被跳过(抛异常时后面的逻辑不执行),afterCompletion 一定执行,类似 finally。

全局异常:@ControllerAdvice 的匹配逻辑

Controller 抛出异常后,doDispatch 捕获并委托 HandlerExceptionResolver 链处理。ExceptionHandlerExceptionResolver 会先在当前 Controller 里找 @ExceptionHandler,找不到再去所有 @ControllerAdvice Bean 里扫。匹配规则按异常类型的最短继承距离选——你同时声明了 ExceptionIllegalStateException 的 handler,抛 IllegalStateException 时走后者,因为继承距离更近。

这个设计让"业务异常转 HTTP 状态码"成为一行注解的事:

java
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public User detail(@PathVariable Long id) {
        if (id <= 0) {
            throw new IllegalArgumentException("id 必须为正数");
        }
        return userService.findById(id); // 找不到会抛 NotFoundException
    }
}

// 全局兜底,任意 Controller 抛的异常都能接住
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResp> badArgs(IllegalArgumentException e) {
        return ResponseEntity.badRequest()
                .body(new ErrorResp(400, e.getMessage()));
    }

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResp> notFound(NotFoundException e) {
        return ResponseEntity.status(404)
                .body(new ErrorResp(404, e.getMessage()));
    }
}

注意 @RestControllerAdvice 组合注解等价于 @ControllerAdvice + @ResponseBody,返回的对象同样走 HttpMessageConverter 转 JSON,不用手动拼字符串。

异步请求:DeferredResult 的价值

Servlet 3.0 之前,一个请求占死一条 Tomcat 工作线程直到响应完成。如果 Controller 里要调一个 3 秒的下游接口,这条线程就干等 3 秒。Tomcat 默认 200 条工作线程,并发一上来就线程耗尽。

DeferredResult 的思路:Controller 立刻返回一个空的 DeferredResult,Tomcat 线程马上释放回池子;等下游结果就绪(比如消息队列回调、另一个线程 setResult()),再由 SpringMVC 派发一次"完成事件",重新走一遍 doDispatch 完成响应。

java
@GetMapping("/async")
public DeferredResult<Result<User>> asyncQuery(@RequestParam Long id) {
    DeferredResult<Result<User>> dr = new DeferredResult<>(5000L); // 5s 超时
    // 交给业务线程池去干等,容器线程立刻归还
    bizExecutor.execute(() -> {
        try {
            dr.setResult(Result.ok(userService.slowFind(id))); // 触发响应
        } catch (Exception e) {
            dr.setErrorResult(Result.fail(e.getMessage()));
        }
    });
    return dr;
}

适用场景是下游慢但连接要保住:支付结果轮询、第三方回调前的占位响应。如果业务本身是 IO 密集且量大,更彻底的方案是 WebFlux 的非阻塞栈,那是另一个话题。

动手实操

把上面几块串成一个可运行的完整例子。建一个 Spring Boot 项目(2.7+ 均可),三个文件:

java
// 1. 拦截器:验证 header 里的 token,并记录请求耗时
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp,
                             Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true; // 静态资源等非方法型 Handler 直接放行
        }
        String token = req.getHeader("X-Token");
        if (token == null || token.isBlank()) {
            resp.setStatus(401);
            resp.getWriter().write("missing token");
            return false; // 短路,Controller 不执行
        }
        req.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse resp,
                                Object handler, Exception ex) {
        Long start = (Long) req.getAttribute("startTime");
        if (start != null) {
            System.out.printf("%s cost %dms, exception=%s%n",
                    req.getRequestURI(), System.currentTimeMillis() - start, ex);
        }
    }
}

// 2. 配置拦截器 + 全局异常
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**")
                .order(1); // 多个拦截器时控制顺序
    }
}

// 3. Controller + 全局异常处理
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public Map<String, Object> detail(@PathVariable Long id,
                                      @RequestHeader("X-Token") String token) {
        if (id <= 0) {
            throw new IllegalArgumentException("id 必须为正数");
        }
        return Map.of("id", id, "status", "PAID", "token", token);
    }
}

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public Map<String, Object> badArgs(IllegalArgumentException e) {
        return Map.of("code", 400, "msg", e.getMessage());
    }
}

断点建议打这四个位置,用 curl 带上 X-Token 请求 /api/orders/1,观察执行顺序:

  1. AuthInterceptor.preHandle() —— 最早的业务逻辑切入点
  2. RequestMappingHandlerAdapter.invokeHandlerMethod() —— 反射调用 Controller 前一刻
  3. RequestResponseBodyMethodProcessor.writeWithMessageConverters() —— Map 转 JSON 的现场
  4. GlobalExceptionHandler.badArgs() —— 把 id 改成 0 再请求一次,看异常如何被接住

常见误区与小结

  • 拦截器 preHandle 返回 false 后,postHandle 不会执行,但已执行过的拦截器的 afterCompletion 会补调用,别在这两个方法里做对称的假设
  • @ControllerAdvice 不是万能兜底:Filter 抛的异常它接不住,因为那时还没进 DispatcherServlet;想接住要在 Filter 层自己 try-catch
  • DeferredResult 释放的是 Tomcat 容器线程,业务线程还得自己出;把慢调用扔到 bizExecutor 只是换了地方等待,吞吐改善来自容器线程不被长占
  • 内容协商失败(客户端 Accept 一个不支持的类型)会抛 HttpMediaTypeNotAcceptableException 返回 406,别和 404 混为一谈
  • Filter 的顺序由 @Order 或注册顺序决定,字符编码 Filter 必须排在写响应的 Filter 前面,否则 setContentType 无效

小结:SpringMVC 的核心是一个 Servlet 入口加一条职责清晰的分发链。HandlerMapping 定位方法,HandlerAdapter 屏蔽处理器差异,ReturnValueHandler + MessageConverter 完成输出,拦截器和 ControllerAdvice 提供横向切面。理解了这条链,排查"参数没绑定上""响应 406""异常没被全局处理"这类问题就能直接定位到链上的具体环节。下一篇讲自动装配:@EnableAutoConfiguration 背后的加载链和条件评估。

参考

参考:Spring Framework 官方文档 Web MVC 章节(DispatcherServlet request processing workflow);《Spring 技术内幕》第 2 版 计文柯;Spring 源码 org.springframework.web.servlet.DispatcherServlet#doDispatch

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1