Spring MVC 请求处理全流程:DispatcherServlet 的九次调用
一个 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);
}
}
把这段代码拆成九步:
checkMultipart:如果是multipart/form-data,包装成MultipartHttpServletRequest。getHandler:遍历所有HandlerMapping,返回第一个非null的HandlerExecutionChain。getHandlerAdapter:遍历所有HandlerAdapter,用supports(handler)找到能处理该处理器的适配器。applyPreHandle:按顺序执行拦截器的preHandle,任一返回false则中断。ha.handle:真正调用 Controller 方法,返回ModelAndView。applyDefaultViewName:无视图名时使用默认视图。applyPostHandle:逆序执行拦截器的postHandle。processDispatchResult:处理异常、渲染视图、触发afterCompletion。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:
ExceptionHandlerExceptionResolver:处理@ExceptionHandler,优先级最高。ResponseStatusExceptionResolver:处理@ResponseStatus与ResponseStatusException。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 // 无视图,直接结束
十、常见坑 / 最佳实践
- 404 先看路径前缀。Spring Boot 3 默认没有
server.servlet.context-path,但网关、Nginx 可能加了前缀。用logging.level.org.springframework.web=DEBUG打开日志,能看到实际匹配的路径。 @RequestBody与@ModelAttribute混用。表单提交用@ModelAttribute(可省略),JSON 提交用@RequestBody。两者混用会导致参数全为null,因为一个读请求体、一个读请求参数。- 拦截器里读请求体导致后续为空。
HttpServletRequest#getInputStream只能读一次。需要多次读取时用ContentCachingRequestWrapper包装,或在Filter层缓存。 @ResponseBody返回String时中文乱码。StringHttpMessageConverter默认字符集在旧版本是 ISO-8859-1,Boot 3 已改为 UTF-8,但仍需确认server.servlet.encoding.charset=UTF-8与force=true。- 拦截器
preHandle返回false后忘记写响应。请求会挂起直到超时,前端表现为"一直转圈"。返回false前必须response.getWriter().write(...)或sendError。 - 异步请求下拦截器行为不同。
@Async或Callable返回值时,preHandle会执行两次(初始与异步分发),postHandle不执行。涉及ThreadLocal的逻辑要在afterConcurrentHandlingStarted中清理。
十一、小结
- 所有请求统一进入
DispatcherServlet#doDispatch,核心是"找 Handler → 找 Adapter → 执行 → 处理结果"。 HandlerMapping在启动时构建RequestMappingInfo注册表,运行期按路径与条件匹配。- 参数解析靠
HandlerMethodArgumentResolver链,返回值写回靠HandlerMethodReturnValueHandler+HttpMessageConverter。 - 拦截器
preHandle正序、postHandle与afterCompletion逆序,postHandle在异常时不执行。 - 异常优先由
@ExceptionHandler处理,兜底是/error的BasicErrorController。
- 点赞
- 收藏
- 关注作者
评论(0)