☰
SpringMVC请求处理流程源码拆解:HandlerMapping与HandlerAdapter深度解析
2026/9/26 7:46:03 网站建设 项目流程

写SpringMVC老项目写了好几年,面试新人或者自己复习底层时,绕不开的核心就是DispatcherServlet如何把一次请求变成对Controller方法的调用。大多数人能背出“HandlerMapping找处理器、HandlerAdapter调处理器”,但真要他说清楚这两个组件在doDispatch方法里是怎么协作的,各自内部又做了哪些事,十有八九会含糊。这篇文章就把这条链路彻底拆开,从源码视角完整走一遍SpringMVC执行流程,重点剖析HandlerMapping和HandlerAdapter的职责与实现,再结合Controller的定位和拦截器切入时机,顺带把那些让人头疼的404、500问题讲透。

1. 核心流程全景:DispatcherServlet里的两行关键代码

1.1 doDispatch方法里的两张王牌

所有SpringMVC请求最终都会进到DispatcherServlet的doDispatch方法里,而整个方法的骨架其实非常短。我把关键部分压缩出来看:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; // 第一步:通过HandlerMapping找到能处理当前请求的执行链 mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // 第二步:通过HandlerAdapter找到能执行该handler的适配器 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 第三步:执行拦截器preHandle if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 第四步:真正调用Controller方法 ha.handle(processedRequest, response, mappedHandler.getHandler()); // 第五步:执行拦截器postHandle,再处理视图 mappedHandler.applyPostHandle(processedRequest, response, mv); processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); }

这里有两个关键点值得注意。mappedHandler不是单纯的Controller对象,而是HandlerExecutionChain,里面装着handler本体和一组拦截器Interceptors。而ha是通过getHandlerAdapter找到的HandlerAdapter实例,它和handler必须是一一匹配的关系。

我调试源码时经常在这几个语句上打条件断点,看mappedHandler到底为不为null、ha到底是哪个实现类、拦截器链里都有谁。这比从日志慢慢猜要快得多。

1.2 一次请求的完整生命周期

从请求进来到响应返回,要经过下面这些环节,每个环节都由明确组件负责:

阶段核心组件说明
请求预处理MultipartResolver判断是不是multipart请求,若是则包装成MultipartHttpServletRequest
查找处理器HandlerMapping返回HandlerExecutionChain,包含handler与拦截器
找到适配器HandlerAdapter遍历全部适配器,用supports方法匹配当前handler
调用前置方法HandlerInterceptor.preHandle执行链从前往后逐个调用,返回false则阻断
业务执行HandlerAdapter.handle解析参数、调用Controller方法、处理返回值
调用后置方法HandlerInterceptor.postHandle视图渲染前执行,能拿到ModelAndView
解析视图ViewResolver把逻辑视图名解析成真实View
渲染响应View.render渲染数据并写出
最后清理HandlerInterceptor.afterCompletion不管是否异常,最终都会执行,适用于释放资源

这里面我特别提醒一下拦截器的两个方法中间,夹着的正是HandlerAdapter的handle调用。也就是说,拦截器拦住的不是“handler方法”,而是整个适配器执行阶段。这个时间点务必记牢,后面讲拦截器时还要展开。

1.3 为什么HandlerMapping与HandlerAdapter必须拆开

很多刚学SpringMVC的人会问:既然最终都是调用某个Controller方法,为什么不直接在DispatcherServlet里写if判断一下路径,然后反射调用对应方法就行了?

原因在于框架的可扩展性。如果DispatcherServlet把所有判断都写在内部,那么每扩展一种处理器类型,就要改一次框架代码。比如传统的Controller接口、基于注解的HandlerMethod、处理静态资源的HttpRequestHandler、甚至WebFlux的函数式路由,它们的类型完全不同,执行方式也截然不同。

拆分之后,两种组件各管一摊:HandlerMapping回答“谁能处理这个URL”,HandlerAdapter回答“这个类型的handler应该怎么被调用”。新增一种handler形态时,只需添加对应的HandlerMapping和HandlerAdapter,DispatcherServlet不需要任何改动。这正是策略模式和适配器模式在框架设计上的典型组合。理解了这个动机,再看后面的源码就不会觉得抽象。

2. HandlerMapping:把URL变成可执行的handler

2.1 查找handler的源码执行细节

实际查找逻辑不在DispatcherServlet里,而是在AbstractHandlerMapping类中。所有主流HandlerMapping实现都继承自这个抽象类,它的getHandler方法定义了一套标准流程:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); if (handler == null) { handler = this.defaultHandler; } if (handler == null) { return null; } if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } if (!(handler instanceof HandlerExecutionChain)) { handler = new HandlerExecutionChain(handler); } // 处理CORS跨域配置 if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) { CorsConfiguration config = getCorsConfiguration(handler, request); // ...合并全局配置和当前handler配置 } // 把全局拦截器加入执行链 if (this.interceptors != null) { for (HandlerInterceptor interceptor : this.interceptors) { chain.addInterceptor(interceptor); } } return chain; }

这段代码里有几个细节非常关键:

第一,getHandlerInternal是模板方法,子类只需负责“根据请求找出对应handler本体”,而包装执行链、处理CORS、挂载拦截器这些通用逻辑全部由父类完成。所以写自定义HandlerMapping时,重点就是实现getHandlerInternal。

第二,handler可以是String类型的bean名称。这一点很多人没注意。UrlBasedViewResolver等场景里,映射值允许填bean名,框架会从容器里解析出真正的bean。

第三,全局拦截器是HandlerMapping在返回前统一挂上去的。所以无论你用哪种HandlerMapping,只要拦截器配置在MappedInterceptor中,都会被塞进执行链。这也是为什么拦截器能够对绝大多数请求生效。

另外,DispatcherServlet在启动时会通过initHandlerMappings收集容器中所有HandlerMapping类型的bean。如果容器里存在多个HandlerMapping,框架会按照order顺序依次调用,拿到第一个非null结果就停止查找。如果全部返回null,才走noHandlerFound逻辑。

2.2 常用HandlerMapping实现类怎么选

SpringMVC默认注册的HandlerMapping有好几个,我整理了它们的核心差异:

实现类匹配方式典型场景使用注意
RequestMappingHandlerMapping扫描@Controller类上的@RequestMapping、@GetMapping等注解注解式MVC,绝大多数项目使用支持Ant风格路径、consumes、produces、params等条件匹配
BeanNameUrlHandlerMapping容器中bean name以/开头的bean早期项目快速把URL指向bean需自己设置order,避免和注解映射冲突
SimpleUrlHandlerMapping通过配置将URL模式映射到handler静态资源、页面跳转、少路由场景适合路由数量固定的项目
RouterFunctionMapping函数式路由,基于RouterFunctionWebFlux风格接口传统MVC项目基本不接触

开发时最常见到的是RequestMappingHandlerMapping。它在初始化时会把容器中所有@Controller或@RestController的bean找出来,再扫描这些bean中的@RequestMapping方法,最终建立“请求条件集合”到HandlerMethod的映射。HandlerMethod内部保存了bean实例、方法对象、参数列表等元数据,但注意它并不是真实调用时的Method对象,而是一个封装层。

查找时,RequestMappingHandlerMapping会把当前请求的路径、请求方法、请求头、参数等条件与注册的映射关系逐一比对。这种多条件匹配能力是SimpleUrlHandlerMapping那种纯路径匹配无法比拟的,也是它能成为主流方案的根本原因。

2.3 自定义一个注解驱动的HandlerMapping

有时候注解满足不了业务需求,比如你希望用一套完全自定义的规则来决定请求交给谁处理。这时候继承AbstractHandlerMapping是最省事的做法。我用一个最简单的Map映射示例说明:

@Component public class CustomHandlerMapping extends AbstractHandlerMapping { private final Map<String, Object> urlHandlerMap = new ConcurrentHashMap<>(); @PostConstruct public void init() { // 假设"/custom/demo"对应一个实现了Controller接口的bean urlHandlerMap.put("/custom/demo", "customController"); // 这个order要小于默认的RequestMappingHandlerMapping的order setOrder(-100); } @Override protected Object getHandlerInternal(HttpServletRequest request) throws Exception { String uri = request.getRequestURI(); String contextPath = request.getContextPath(); if (StringUtils.hasText(contextPath)) { uri = uri.substring(contextPath.length()); } Object handler = urlHandlerMap.get(uri); if (handler instanceof String) { handler = obtainApplicationContext().getBean((String) handler); } return handler; } }

这段代码里有几个坑必须提醒:

坑一:contextPath一定要手动去掉,因为getRequestURI返回的是包含项目上下文的完整路径。不处理的话,带contextPath部署后映射永远匹配不上。

坑二:返回的handler必须是框架认识的类型。上面示例中我特意说“实现了Controller接口的bean”,因为SimpleControllerHandlerAdapter能处理这种类型。如果你返回一个带有@Controller注解的普通bean,而它又没有匹配的@RequestMapping路径,就会因为没有适配器而报No adapter for handler错误。

坑三:setOrder的数值决定查找顺序,一般要设为负数或者比默认映射器更小的值,这样框架会先问你的自定义映射器,查不到再落到Spring的注解映射。

我自己做这类扩展时,还会在init方法里把自定义规则输出到日志,方便排查路径匹配问题。这个习惯建议保留,因为自定义映射一旦出了问题,排查成本比注解方式高很多。

3. HandlerAdapter:适配器怎么让handler真正跑起来

3.1 supports方法决定谁接单

HandlerAdapter的接口定义非常精简:

public interface HandlerAdapter { boolean supports(Object handler); ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; long getLastModified(HttpServletRequest request, Object handler); }

DispatcherServlet里的getHandlerAdapter方法核心逻辑是遍历容器中所有HandlerAdapter,按顺序调用supports方法,第一个返回true的适配器获得执行权。之所以用遍历匹配而不是直接写死,正因为handler类型是开放集合。

我们常见的适配器和handler类型的对应关系如下:

HandlerAdapter实现类支持的handler类型典型场景
RequestMappingHandlerAdapterHandlerMethod(注解方法)所有基于@Controller的方法
SimpleControllerHandlerAdapter实现Controller接口的bean旧式Controller接口
HttpRequestHandlerAdapter实现HttpRequestHandler接口的bean静态资源处理、自定义请求处理
HandlerFunctionAdapterHandlerFunction(函数式)WebFlux风格

以SimpleControllerHandlerAdapter为例,它的supports实现简单到让人意外:

public boolean supports(Object handler) { return (handler instanceof Controller); }

也就是说,如果handler实现了org.springframework.web.servlet.mvc.Controller接口,SimpleControllerHandlerAdapter就认为自己能接单。匹配规则就是这么直接。

3.2 RequestMappingHandlerAdapter内部做了什么

这是最核心的适配器,因为注解式Controller走的就是它。它的实际调用链路大致是:

ha.handle(request, response, handler) -> handleInternal -> invokeHandlerMethod -> ServletInvocableHandlerMethod.invokeAndHandle -> 解析方法参数 -> 反射调用Controller方法 -> 处理返回值

参数解析是第一步重头戏。Spring内置了二十多个HandlerMethodArgumentResolver,每个resolver负责解析一种或多种参数类型。调用逻辑很直接:逐一判断resolver.supportsParameter,找到第一个支持当前参数的就执行解析。

比如@RequestBody参数,最终由RequestResponseBodyMethodProcessor负责,它会找到HttpMessageConverter,从请求体里读取字节流并反序列化成对象。@PathVariable则是直接到容器里拿已匹配的路径片段。我之前遇到过参数不生效的问题,基本都是因为用了自定义注解而忘了注册对应的解析器,导致Spring找不到能处理这个注解的程序,最终把参数当成了null。

返回值处理是第二步重头戏。如果方法上有@ResponseBody注解,返回值会交给RequestResponseBodyMethodProcessor,选择合适的HttpMessageConverter转换成JSON或XML写入响应体。此时ModelAndView实际上是空的,因为数据不是通过视图渲染出去的。

没有@ResponseBody注解时,返回值会作为Model属性放进ModelAndViewContainer,同时解析出视图名,最后构建成ModelAndView交给ViewResolver。这套机制解释了为什么同一个Controller方法,返回字符串时既可以表示数据,也可以表示逻辑视图名,区别全在返回值处理器怎么解释它。

3.3 手写HandlerAdapter接入自己的处理器

自定义HandlerAdapter一般出现在框架级封装里,但弄懂它的写法能加深理解。我写一个简单示例:

public class MyHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return handler instanceof MyHandler; } @Override public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { MyHandler myHandler = (MyHandler) handler; myHandler.doHandle(request, response); return null; } @Override public long getLastModified(HttpServletRequest request, Object handler) { return -1; } }

然后通过WebMvcConfigurer注入进去:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void extendHandlerAdapters(List<HandlerAdapter> adapters) { adapters.add(0, new MyHandlerAdapter()); } }

注意我用了add(0, ...)把适配器加到列表最前面。这样当自定义handler出现时,能优先被匹配。如果加到列表尾部,碰到类型不明确的handler时,可能轮不到你的适配器执行。

这套扩展机制在生产环境最大的价值在于,你可以完全绕开SpringMVC的Controller模型,接入自己的路由引擎、脚本引擎或协议处理器。我见过某些网关系统通过自定义HandlerMapping和HandlerAdapter整合了规则引擎,让请求直接走规则流而不经过Controller方法,灵活性非常高。

4. Controller与拦截器在整个流程中的位置

4.1 Controller在SpringMVC中到底是什么

很多初学者以为“找到Controller”就是拿到了Controller对象,其实在注解式开发中,HandlerMapping返回的handler是HandlerMethod对象,它是对Controller类的某个方法进行包装后的产物,包含了方法对象、所在bean实例、参数名等信息。真正触发业务逻辑的是HandlerAdapter调用HandlerMethod。

Controller本身也有新旧之分:

风格定义方式适配器特点
注解式@Controller + @RequestMapping、@GetMapping等RequestMappingHandlerAdapter灵活、主流、支持参数绑定和返回处理
接口式实现Controller接口,重写handleRequestSimpleControllerHandlerAdapter简单、适用于极早版本项目
HttpRequestHandler实现HttpRequestHandler接口HttpRequestHandlerAdapter适合文件下载、流式输出等场景

接口式Controller现在很少见了,但理解它有助于理解适配器模式。以前写一个页面跳转就是实现Controller接口,然后配置SimpleUrlHandlerMapping把URL映射到这个bean。而现在大家都是写一个类标注@Controller,方法标上@RequestMapping,连XML都不用改。两种方式最终都会走到同一个Adapter模式上,只是包装形式不同。

Controller自身的线程安全问题值得提醒:Controller默认是单例,多个并发请求共用同一个实例。只要不在Controller里保存有状态的可变字段,一般没问题。如果在字段里放了某个请求专有的状态,并发下就会出现数据串味。因为Controller本质是Spring容器中的bean,它没有每个请求一个实例的机制。

4.2 拦截器三个方法与调用时机

拦截器接口HandlerInterceptor定义了三个默认方法:

  • preHandle:handler执行前调用,返回false可阻断请求。
  • postHandle:handler执行后、视图渲染前调用,可以修改ModelAndView。
  • afterCompletion:整个请求结束后调用,通常用于清理资源。

它们在HandlerExecutionChain里的执行顺序非常清晰。preHandle是从链头向链尾执行,只要有一个返回false,后续拦截器的preHandle和所有拦截器的postHandle都不执行,但已经执行过preHandle的拦截器会反向触发afterCompletion。而postHandle和afterCompletion都是从链尾向链头反向执行,我列出这个顺序是为了排查日志时序时不被绕晕。

一个典型的日志拦截器是这样写的:

@Component public class AccessLogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { long start = System.currentTimeMillis(); request.setAttribute("startTime", start); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { long start = (long) request.getAttribute("startTime"); long cost = System.currentTimeMillis() - start; System.out.println("接口耗时:" + cost + "ms"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { System.out.println("请求完成"); } }

注册时只要在WebMvcConfigurer里重写addInterceptors即可:

registry.addInterceptor(accessLogInterceptor).addPathPatterns("/api/**");

拦截器和Filter的差别也常被问到。Filter是Servlet规范层面的组件,在DispatcherServlet之前执行,拿不到handler信息;拦截器是SpringMVC层面的组件,执行时已经能明确当前被调用的handler是哪个。如果要做权限校验,拦截器比Filter更合适,因为你能拿到具体的Controller方法做细粒度判断。

4.3 前后端分离下为什么拦截器拿不到ModelAndView

这个问题我见过很多人踩。写前后端分离项目时,Controller方法上加@ResponseBody返回JSON,结果是postHandle里收到的modelAndView是null,有人因此误以为拦截器没生效,浪费了大量时间定位。

原因在于返回值处理机制。当方法标注@ResponseBody时,RequestResponseBodyMethodProcessor处理完返回值后,会调用mavContainer.setRequestHandled(true)。这个标记语义是“响应已经由数据转换器直接写回,不需要再走视图渲染”。于是invokeHandlerMethod结束后构建ModelAndView时发现请求已处理,就直接返回null了。

所以postHandle里modelAndView为null,不代表拦截器没执行,而是数据已经直接写出。如果你需要在返回JSON的场景里记录日志,不要把口径放在postHandle的ModelAndView上,直接在preHandle和afterCompletion里记录时间更可靠。

异步请求的时序也有类似差异。启用异步处理后,preHandle直接返回true,DispatcherServlet会退出;等业务线程完成异步结果时,才会再次触发afterCompletion。这个场景下不能用传统同步时序去理解拦截器调用次数。

5. 高频问题与排查手记

5.1 写了@Controller却还是404的排查路径

404是最常见的问题,我按排查优先级整理了一套路径:

第一步,看启动日志中是否输出了“Mapped”开头的请求映射信息。SpringMVC启动时会把每个注册的路由打印出来,看到你写的路径再继续下一步。

第二步,看访问路径与@RequestMapping的拼接是否一致。很多项目Controller类上有@RequestMapping(“/user”),方法上有@GetMapping(“/list”),实际访问路径是“/user/list”。漏掉类级前缀是最常见失误。

第三步,在getHandler方法处打断点,看mappedHandler是否为null。如果为null,说明HandlerMapping环节就没匹配上,根本还没到Controller调用阶段。

第四步,检查包扫描是否覆盖到Controller所在包。SpringBoot启动类一般会扫描其子包,Controller放在出了子包的位置后,类根本没注册成bean。

第五步,排查是不是自定义HandlerMapping把order设得太靠前,提前返回了一个null之外的自定义映射,把默认映射挤掉了。这种问题通常发生在引入第三方组件后突然大量404时。

5.2 No adapter for handler异常怎么追

日志里出现“No adapter for handler”时,说明handler已经找到了,但没有任何一个HandlerAdapter认识它。最典型的写法是在XML里配置了一个普通bean,然后通过SimpleUrlHandlerMapping把它当作handler映射出去。普通bean是Object类型,适配器列表里没有谁能支持Object,于是抛出这个异常。

排查时可以看一下mappedHandler.getHandler()的实际类型。如果它既不是HandlerMethod、不是Controller接口实现、也不是HttpRequestHandler实现,那就要回HandlerMapping配置里检查映射源是否正确。我见过有人在SimpleUrlHandlerMapping里把URL映射到了字符串“/index”,结果框架把字符串当作bean名去找,找回来的对象没有实现任何受支持的接口,当场报错。

5.3 参数绑定400和返回乱码的现场排障

参数绑定失败时最常见的状态码就是400。比如Controller方法接收Integer类型参数,请求却传了“abc”,类型转换直接失败。这时会抛出TypeMismatchException,原因是前端传参格式与Java类型不匹配。

Date类型是另一个高发点。Spring内置的日期转换默认不支持“yyyy-MM-dd HH:mm:ss”之外的自定义格式,需要配置@DateTimeFormat或者注册全局Converter。我记得最清楚的一个案例是前端传了时间戳字符串,后端用Date接收,排查了很久才发现需要自己写一个String转Date的转换器。

响应乱码问题则主要集中在StringHttpMessageConverter上。老版本Spring中它的默认字符集是ISO-8859-1,返回中文会乱码。解决办法是显式设置produces为application/json;charset=UTF-8,或者直接替换消息转换器。排查乱码时先看响应头里的Content-Type带不带charset=utf-8,这是最快区分问题方向的手段。

5.4 Ambiguous mapping多方法映射冲突

启动时如果日志抛出“Ambiguous mapping”异常,说明两个方法映射到了同一个请求条件上。最常见于同名重载方法:一个类里两个方法都标了@GetMapping(“/same”),Spring无法判断到底该用哪个,直接拒绝启动。

排查方法是找异常信息里包含的两个类的类名和方法名,看它们是否确实共享了同一个路径和请求方法组合。如果一个是get一个是post,框架能区分,不会报错;但两个get必然冲突。处理方案就是改路径、改请求方式,或者合并成一个方法。

我遇到过一个隐蔽场景:Controller方法上用了@GetMapping,类上没有路径,而另一个Controller的方法路径恰好在同一根路径下,两个都有通配符,最终也被判定为冲突。这种通配符重叠问题比完全相同的路径更难发现,需要把RequestMapping条件整体打印出来逐一对比。

这些年排查下来,我最大的体会是:真正吃透SpringMVC执行流程,不需要把源码逐行背下来,但一定要把组件职责、调用顺序和接口约定这三件事记牢。拿到一个诡异问题,先确认是卡在HandlerMapping的查找阶段,还是卡的HandlerAdapter的执行阶段,方向对了,问题就解决了一半。我对新人的建议是,遇到请求问题先别急着换框架加依赖,回到doDispatch里给getHandler和handle各打一个条件断点,看看mappedHandler到底找到没找到、适配器到底是哪个实现类,事实往往比你推测的简单得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询