Spring MVC 请求处理全流程:DispatcherServlet 的九次调用

举报
yd_94907100 发表于 2026/11/01 21:26:40 2026/11/01
【摘要】 沿 doDispatch 逐行拆解一次 HTTP 请求的完整处理链。

一个 GET /api/users/1 从进入 Tomcat 到返回 JSON,中间经过哪些类、哪些方法?大多数人能说出 DispatcherServlet 和 @RestController,但说不清 HandlerMapping 怎么找到方法、HandlerAdapter 怎么把字符串转成 Long、@ResponseBody 又是在哪一步写回响应。本文沿着 DispatcherServlet#doDispatch 逐行拆解,Spring Framework 6.x / Spring Boot 3.x 为准。适合想排查 404、参数绑定失败、拦截器顺序问题的后端开发阅读。

一、整体结构

Spring MVC 的核心组件及其职责:

| 组件 | 职责 |

| --- | --- |

| DispatcherServlet | 前端控制器,统一入口,协调其余组件 |

| HandlerMapping | 根据请求找到处理器(方法)与拦截器链 |

| HandlerAdapter | 调用处理器,适配不同类型的处理器 |

| HandlerExceptionResolver | 处理异常,生成响应 |

| HandlerInterceptor | 请求前后的横切逻辑 |

| HttpMessageConverter | 请求体反序列化、响应体序列化 |

| ViewResolver | 视图名解析为 View(前后端分离场景基本不用) |

在 Spring Boot 3 中,DispatcherServletAutoConfiguration 会注册一个 DispatcherServlet Bean,路径默认为 /,由 ServletWebServerFactory 创建的内嵌 Tomcat 加载。

二、doDispatch 的完整调用链

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            dispatchException = new ServletException("Handler dispatch failed: " + err, err);
        }
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    finally {
        cleanupMultipart(processedRequest, response, multipartRequestParsed);
    }
}

把这段代码拆成九步:

  1. checkMultipart:如果是 multipart/form-data,包装成 MultipartHttpServletRequest。
  2. getHandler:遍历所有 HandlerMapping,返回第一个非 null 的 HandlerExecutionChain。
  3. getHandlerAdapter:遍历所有 HandlerAdapter,用 supports(handler) 找到能处理该处理器的适配器。
  4. applyPreHandle:按顺序执行拦截器的 preHandle,任一返回 false 则中断。
  5. ha.handle:真正调用 Controller 方法,返回 ModelAndView。
  6. applyDefaultViewName:无视图名时使用默认视图。
  7. applyPostHandle:逆序执行拦截器的 postHandle。
  8. processDispatchResult:处理异常、渲染视图、触发 afterCompletion。
  9. cleanupMultipart:清理临时文件。

三、HandlerMapping 如何找到方法

RequestMappingHandlerMapping 在启动时扫描所有 @Controller/@RestController,为每个 @RequestMapping 方法构建一个 RequestMappingInfo,并注册到 MappingRegistry:

class MappingRegistry {
    private final Map<RequestMappingInfo, MappingRegistration<?>> registry = new HashMap<>();
    private final Map<String, MappingRegistration<?>> mappingLookup = new HashMap<>();
    private final Map<RequestMappingInfo, HandlerMethod> mappingLookup = new LinkedHashMap<>();
    private final MultiValueMap<String, String> pathLookup = new LinkedMultiValueMap<>();
    private final List<RequestMappingInfo> directPathMatches = new ArrayList<>();
}

RequestMappingInfo 封装了路径、HTTP 方法、参数、请求头、consumes、produces 等条件。查找时 AbstractHandlerMethodMapping#lookupHandlerMethod 先用 pathLookup 做直接路径匹配,再对带变量的路径做模式匹配,最后按 RequestMethodsRequestCondition、ParamsRequestCondition 等条件排序取最优。

这就是为什么两个 Controller 声明了相同路径但不同 HTTP 方法不会冲突,而路径和方法都相同时启动直接报:

Ambiguous mapping. Cannot map 'userController' method
com.example.UserController#get(Long)
to {GET [/api/users/{id}]}: There is already 'userController' bean method

四、HandlerAdapter 如何调用方法

RequestMappingHandlerAdapter#handleInternal 调用 invokeHandlerMethod,内部构造 ServletInvocableHandlerMethod 并执行:

protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
        HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    ServletInvocableHandlerMethod invocableMethod =
            createInvocableHandlerMethod(handlerMethod);
    invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);

    WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
    ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

    invocableMethod.invokeAndHandle(webRequest, mavContainer);
    // ...
}

4.1 参数解析

InvocableHandlerMethod#getMethodArgumentValues 遍历方法参数,为每个参数找一个 HandlerMethodArgumentResolver:

for (int i = 0; i < parameters.length; i++) {
    MethodParameter parameter = parameters[i];
    if (!this.resolvers.supportsParameter(parameter)) {
        throw new IllegalStateException(...);
    }
    args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, binderFactory);
}

常用解析器及其处理的注解:

| 解析器 | 处理 |

| --- | --- |

| RequestParamMethodArgumentResolver | @RequestParam、简单类型 |

| PathVariableMethodArgumentResolver | @PathVariable |

| RequestBodyAdvice 链 + RequestResponseBodyMethodProcessor | @RequestBody |

| ServletModelAttributeMethodProcessor | @ModelAttribute、表单对象 |

| RequestHeaderMethodArgumentResolver | @RequestHeader |

| ServletCookieValueMethodArgumentResolver | @CookieValue |

@PathVariable Long id 的 String → Long 转换由 WebDataBinder 内的 ConversionService 完成。转换失败抛 MethodArgumentTypeMismatchException,默认返回 400。Spring Boot 3 默认使用 ApplicationConversionService,支持 ISO 日期、枚举忽略大小写等。

4.2 返回值处理

HandlerMethodReturnValueHandler 负责把返回值写回响应。@ResponseBody(含 @RestController)由 RequestResponseBodyMethodProcessor 处理,它遍历 HttpMessageConverter,根据 Accept 头和返回类型选择 MappingJackson2HttpMessageConverter 完成序列化。

HttpMessageConverter 的选择顺序在 Spring Boot 3 中由 WebMvcAutoConfiguration 决定,Jackson 排在前列。String 类型优先用 StringHttpMessageConverter,这就是为什么返回 String 时中文可能乱码而返回对象不会。

五、拦截器与过滤器

| 维度 | Filter | Interceptor |

| --- | --- | --- |

| 规范 | Servlet 规范 | Spring MVC 规范 |

| 作用范围 | 所有请求,包括静态资源 | 只作用于 DispatcherServlet 处理的请求 |

| 能否拿到 HandlerMethod | 不能 | 能,preHandle 的第三个参数 |

| 依赖容器 | 需要 Servlet 容器 | 不需要,可脱离容器测试 |

| 典型用途 | 编码、CORS、请求包装 | 登录校验、权限、日志 |

拦截器的注册:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/login", "/api/health")
                .order(1);
    }
}

preHandle 正序执行,postHandle 与 afterCompletion 逆序执行,形成栈式结构。postHandle 在异常时不会执行,afterCompletion 无论成功失败都会执行,适合做资源清理。

六、异常处理链路

processDispatchResult 中,如果有异常,调用 processHandlerException,遍历 HandlerExceptionResolver:

  1. ExceptionHandlerExceptionResolver:处理 @ExceptionHandler,优先级最高。
  2. ResponseStatusExceptionResolver:处理 @ResponseStatus 与 ResponseStatusException。
  3. DefaultHandlerExceptionResolver:处理 Spring MVC 内置异常,如 MethodArgumentNotValidException 默认返回 400。

三者都不处理时,异常抛回 Servlet 容器,跳转到 /error,由 BasicErrorController 生成默认错误响应。这也是为什么没配全局异常处理时,前端会收到 Boot 的默认 JSON 结构。

七、异步请求与流式响应

Spring MVC 支持三种异步返回类型,它们的处理路径与同步请求不同:

  • Callable<T>:由 WebAsyncManager 交给 TaskExecutor 执行,Servlet 容器线程立即释放。请求再次分发时会重新经过拦截器链。
  • DeferredResult<T>:由业务线程在任意时刻调用 setResult 完成,适合等待外部事件(如 MQ 回调)。
  • ResponseBodyEmitter / SseEmitter:流式写出,适合 SSE 推送。
@GetMapping("/report")
public DeferredResult<ReportVO> generate() {
    DeferredResult<ReportVO> result = new DeferredResult<>(30_000L);
    result.onTimeout(() -> result.setErrorResult(
            new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, "生成超时")));
    executor.execute(() -> {
        try {
            result.setResult(reportService.generate());
        } catch (Exception ex) {
            result.setErrorResult(ex);
        }
    });
    return result;
}

异步请求的关键细节:doDispatch 在 ha.handle 之后检查 asyncManager.isConcurrentHandlingStarted(),为真时直接返回,跳过 postHandle。请求再次分发时,preHandle 会重新执行一次,afterCompletion 只在最终完成时执行。涉及 ThreadLocal 的逻辑(如 MDC、安全上下文)必须在 afterConcurrentHandlingStarted 中清理,否则会污染容器线程。

八、路径匹配:AntPathMatcher 与 PathPattern

Spring 5.3 引入 PathPatternParser,Spring 6 起它成为 MVC 的默认策略,AntPathMatcher 退居兼容位置。两者在通配符语义上有实质差异:

| 语法 | AntPathMatcher | PathPattern |

| --- | --- | --- |

| * | 匹配单层路径中的任意字符 | 匹配单层路径中的任意字符 |

| ** | 只能出现在末尾 | 可出现在中间,但只能有一段 |

| {id:[0-9]+} | 支持正则 | 支持正则 |

| {*path} | 不支持 | 捕获剩余全部路径 |

| 匹配性能 | 每次请求重新解析 | 启动时解析为链式结构,复用 |

PathPattern 在启动时把模式解析成 PathElement 链,匹配时逐段比较,避免了 AntPathMatcher 的字符串切分开销,高并发下差异明显。

另一个 Boot 3 的变化是尾斜杠匹配默认关闭。/api/users/ 不再匹配 @GetMapping("/api/users"),需要 spring.mvc.trailing-slash-match=true 才能恢复旧行为。升级后出现莫名 404 时,这是首先要排查的点。

九、一个完整示例

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

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public UserVO get(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Long create(@Valid @RequestBody UserCreateDTO dto) {
        return userService.create(dto);
    }
}

请求 GET /api/users/1 的完整路径:

Tomcat → FilterChain → DispatcherServlet#service
  → doDispatch
    → RequestMappingHandlerMapping#getHandler        // 匹配 GET /api/users/{id}
    → RequestMappingHandlerAdapter#handle
      → InvocableHandlerMethod#invoke
        → PathVariableMethodArgumentResolver         // "1" → Long 1
        → UserController#get
        → RequestResponseBodyMethodProcessor         // UserVO → JSON
    → processDispatchResult                          // 无视图,直接结束

十、常见坑 / 最佳实践

  1. 404 先看路径前缀。Spring Boot 3 默认没有 server.servlet.context-path,但网关、Nginx 可能加了前缀。用 logging.level.org.springframework.web=DEBUG 打开日志,能看到实际匹配的路径。
  2. @RequestBody 与 @ModelAttribute 混用。表单提交用 @ModelAttribute(可省略),JSON 提交用 @RequestBody。两者混用会导致参数全为 null,因为一个读请求体、一个读请求参数。
  3. 拦截器里读请求体导致后续为空。HttpServletRequest#getInputStream 只能读一次。需要多次读取时用 ContentCachingRequestWrapper 包装,或在 Filter 层缓存。
  4. @ResponseBody 返回 String 时中文乱码。StringHttpMessageConverter 默认字符集在旧版本是 ISO-8859-1,Boot 3 已改为 UTF-8,但仍需确认 server.servlet.encoding.charset=UTF-8 与 force=true。
  5. 拦截器 preHandle 返回 false 后忘记写响应。请求会挂起直到超时,前端表现为"一直转圈"。返回 false 前必须 response.getWriter().write(...) 或 sendError。
  6. 异步请求下拦截器行为不同。@Async 或 Callable 返回值时,preHandle 会执行两次(初始与异步分发),postHandle 不执行。涉及 ThreadLocal 的逻辑要在 afterConcurrentHandlingStarted 中清理。

十一、小结

  • 所有请求统一进入 DispatcherServlet#doDispatch,核心是"找 Handler → 找 Adapter → 执行 → 处理结果"。
  • HandlerMapping 在启动时构建 RequestMappingInfo 注册表,运行期按路径与条件匹配。
  • 参数解析靠 HandlerMethodArgumentResolver 链,返回值写回靠 HandlerMethodReturnValueHandler + HttpMessageConverter。
  • 拦截器 preHandle 正序、postHandle 与 afterCompletion 逆序,postHandle 在异常时不执行。
  • 异常优先由 @ExceptionHandler 处理,兜底是 /error 的 BasicErrorController。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。