Spring MVC HandlerAdapter 源码解析:适配器模式与请求处理机制
1. 从一次接口 404 的排查说起为什么我要啃 HandlerAdapter 源码前段时间帮一个朋友排查他项目里的问题现象很怪Controller 类写好了RequestMapping路径也确认没写错启动日志里能看到映射注册成功但请求打过去就是 404。他反复检查了包扫描路径、注解、启动类位置甚至把 Spring Boot 版本降了一级问题依旧。最后定位到的原因是他自定义了一个HandlerMapping返回的HandlerExecutionChain里塞的 handler 类型和容器里默认注册的HandlerAdapter对不上号——映射找到了但没有适配器能驱动这个 handler 执行于是框架直接判定为不可处理对外表现就是 404。这个坑让我意识到一件事大多数人学 Spring MVC 的请求处理流程注意力都放在DispatcherServlet和HandlerMapping上知道请求进来先找映射但真正决定这个 handler 到底怎么被调用、参数怎么绑定、返回值怎么处理的其实是HandlerAdapter。它是整个 MVC 执行链路里最容易被忽略、却最不能出错的一环。你写的每一个RequestMapping方法、每一个Controller最终都是被某个HandlerAdapter实现类反射调用起来的。这篇内容我打算把HandlerAdapter这个接口从源码层面拆开讲透。不是那种贴一段接口定义就完事的泛泛之谈而是围绕三个问题展开这个接口到底定义了哪几个方法、每个方法在什么时机被谁调用、以及 Spring 内置的几个实现类分别适配什么类型的 handler。中间会穿插我自己读源码和踩坑过程中总结的判断逻辑包括参数解析器、返回值处理器是怎么和适配器协作的以及自定义 handler 时最容易翻车的地方。适合已经会用 Spring MVC、但想搞清楚底层执行机制的同学也适合正在做框架二次开发、需要自定义适配器的人。先把结论放前面HandlerAdapter是一个典型的策略接口它把如何调用一个 handler这件事从DispatcherServlet里抽离出来让不同类型的 handler注解方法、普通 Servlet、函数式端点等都能用统一的方式被执行。理解它的关键不在于记住接口方法签名而在于理解适配这两个字在框架设计里的分量。2. HandlerAdapter 接口的四个方法每一个都在解决一个具体问题2.1 supports 方法适配器的准入判断HandlerAdapter接口本身非常精简核心方法就那么几个。第一个要说的就是supports(Object handler)它的签名是返回 boolean入参是任意 handler 对象。这个方法的作用只有一个告诉DispatcherServlet这个 handler 我能不能处理。为什么需要这个方法因为DispatcherServlet在拿到HandlerExecutionChain之后手里只有一个Object类型的 handler它并不知道这玩意儿是个HandlerMethod、是个HttpRequestHandler、还是个Servlet。它唯一能做的就是遍历容器里所有的HandlerAdapter挨个问一句你能处理吗第一个回答 true 的就被选中然后由它负责后续的调用。这里有个细节值得注意DispatcherServlet里维护的是一个ListHandlerAdapter遍历顺序就是 Bean 的注册顺序。默认情况下RequestMappingHandlerAdapter、HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter这些是按固定顺序注册的。如果你自定义了一个适配器并且它的supports方法写得过于宽泛比如对所有 handler 都返回 true那它很可能抢在默认适配器前面被选中导致原本能正常工作的请求全部走你的逻辑。我见过有人写了个万能适配器做日志埋点结果把RequestMapping方法的执行全接管了参数绑定直接失效排查了大半天。所以supports方法的实现原则很明确判断条件要尽可能精确只认自己真正能处理的类型。比如RequestMappingHandlerAdapter的判断逻辑就是handler instanceof HandlerMethod干净利落绝不误伤。2.2 handle 方法真正干活的入口第二个核心方法是handle(HttpServletRequest request, HttpServletResponse response, Object handler)返回ModelAndView。这是适配器真正执行 handler 的地方也是整个接口里最重的方法。它的职责链条其实很长调用 handler、处理参数绑定、执行目标方法、处理返回值、封装ModelAndView。以RequestMappingHandlerAdapter为例handle内部会先通过ServletInvocableHandlerMethod把 handler 包装成一个可调用的对象然后依次调用参数解析器链HandlerMethodArgumentResolverComposite解析出方法入参反射执行目标方法最后交给返回值处理器链HandlerMethodReturnValueHandlerComposite处理返回值。这里有个设计上的巧妙之处handle方法返回的是ModelAndView但并不是所有 handler 都需要返回视图。比如ResponseBody标注的方法返回值直接写进响应体了此时返回的ModelAndView就是 null。DispatcherServlet拿到 null 之后会跳过视图渲染阶段直接结束请求。这种返回 null 表示无需视图的约定是适配器和前端控制器之间的一个隐性契约读源码时如果不注意很容易困惑为什么这里返回 null 还能正常工作。另外要提醒一点handle方法签名里抛出了Exception。这意味着适配器内部执行 handler 时产生的异常会向上抛给DispatcherServlet再由processHandlerException交给HandlerExceptionResolver处理。所以你在ExceptionHandler里能捕获到的异常本质上是从适配器的handle方法里抛出来的。理解这条链路对排查异常处理不生效的问题很有帮助。2.3 getLastModified 方法被大多数人忽略的缓存优化第三个方法是getLastModified(HttpServletRequest request, Object handler)返回 long 类型的时间戳。这个方法默认实现直接返回 -1表示不支持 last-modified 缓存机制。它的作用是配合 HTTP 的Last-Modified/If-Modified-Since头做浏览器缓存。如果适配器返回了一个有效的时间戳DispatcherServlet会把它设置到响应的Last-Modified头里下次浏览器带着If-Modified-Since请求过来时框架会比对时间戳如果没变化就直接返回 304跳过 handler 执行。绝大多数场景下这个方法用不上因为现代 Web 应用的缓存策略基本都交给前端资源服务器或者专门的缓存层了。但在一些静态内容动态生成的场景里比如根据数据库内容渲染的页面用这个方法能省掉不少重复计算。HttpRequestHandlerAdapter和SimpleControllerHandlerAdapter都实现了这个方法会去调用 handler 自身的getLastModified逻辑。而RequestMappingHandlerAdapter默认返回 -1因为注解方法的缓存语义不明确框架不敢擅自做主。2.4 接口方法总览与调用时机对照把三个方法放在一起看能更清楚地理解它们各自的定位方法名入参返回值调用时机核心职责supportsObject handlerboolean映射查找完成后选择适配器时判断能否处理该 handlerhandlerequest, response, handlerModelAndView适配器选定后执行 handler 时参数绑定、方法调用、返回值处理getLastModifiedrequest, handlerlonghandle 执行前缓存判断时提供 last-modified 时间戳这三个方法的调用顺序是固定的先supports筛选再getLastModified判断缓存最后handle执行。DispatcherServlet的doDispatch方法里这段逻辑写得非常清晰建议对照源码读一遍比看任何二手资料都管用。3. 内置适配器实现类谁在适配什么样的 handler3.1 RequestMappingHandlerAdapter注解驱动的主力这是日常开发中出场率最高的适配器没有之一。你写的所有ControllerRequestMapping方法最终都是被它执行的。它的supports判断条件是handler instanceof HandlerMethod而HandlerMethod正是RequestMappingHandlerMapping在扫描注解方法时封装出来的对象。RequestMappingHandlerAdapter的内部结构相当复杂它持有几个关键组件HandlerMethodArgumentResolverComposite参数解析器组合、HandlerMethodReturnValueHandlerComposite返回值处理器组合、WebBindingInitializer数据绑定初始化器、ConversionService类型转换服务。这些组件在afterPropertiesSet阶段被初始化然后在每次handle调用时协同工作。参数解析这块值得单独说。RequestParam、PathVariable、RequestBody、ModelAttribute这些注解之所以能生效全靠对应的HandlerMethodArgumentResolver实现类。框架在启动时会注册一长串解析器handle执行时按顺序遍历找到第一个supportsParameter返回 true 的解析器来解析当前参数。这个责任链模式的好处是扩展性极强你想支持一种新的参数注解只需要实现一个解析器并注册进去就行完全不用改适配器本身。返回值处理同理。ResponseBody对应RequestResponseBodyMethodProcessor返回ModelAndView对应ModelAndViewMethodReturnValueHandler返回String表示视图名对应ViewNameMethodReturnValueHandler。这些处理器同样按顺序匹配第一个能处理的就负责到底。3.2 HttpRequestHandlerAdapter适配无视图的处理器HttpRequestHandlerAdapter适配的是HttpRequestHandler接口的实现类。这个接口只有一个handleRequest方法没有返回值也不涉及视图渲染典型用途是处理静态资源或者做一些纯 IO 操作。它的supports判断是handler instanceof HttpRequestHandlerhandle方法直接调用handleRequest然后返回 null 表示无需视图。getLastModified会委托给 handler 自身的实现。这个适配器结构简单但它的存在说明了一个设计理念框架不强迫所有 handler 都走注解那一套只要你能被某个适配器识别就能接入请求处理流程。3.3 SimpleControllerHandlerAdapter老式 Controller 的兼容层SimpleControllerHandlerAdapter适配的是实现了Controller接口的类这是 Spring MVC 早期版本的编程式写法。Controller接口的handleRequest方法返回ModelAndView适配器直接调用它并返回结果。现在很少有人这么写了但这个适配器依然保留着用于兼容老代码。它的supports判断是handler instanceof Controller。有意思的是Controller注解标注的类并不实现Controller接口所以它不会被这个适配器选中而是走RequestMappingHandlerAdapter。这两个名字很像但适配的目标完全不同初学源码时特别容易混淆。3.4 函数式端点适配器新范式的接入方式Spring 5 之后引入了函数式端点Functional Endpoints的编程模型对应的 handler 类型是HandlerFunction。适配这类 handler 的是HandlerFunctionAdapter它的supports判断是handler instanceof HandlerFunctionhandle方法会调用函数并处理ServerResponse。这个适配器的出现进一步印证了HandlerAdapter接口的扩展价值每当框架引入一种新的 handler 类型只需要新增一个适配器实现就能无缝接入现有的请求处理流程DispatcherServlet一行代码都不用改。这就是策略模式在框架设计里的威力。4. 适配器与参数解析、返回值处理的协作链路4.1 一次 handle 调用内部到底发生了什么很多人以为handle方法就是简单反射调用一下目标方法实际上远不止。以RequestMappingHandlerAdapter为例一次完整的handle调用大致经历这几个阶段第一阶段是准备ServletWebRequest把原生的 request 和 response 包装成框架内部的请求对象方便后续统一操作。第二阶段是创建ServletInvocableHandlerMethod把HandlerMethod和参数解析器、返回值处理器、数据绑定器组装到一起。第三阶段是调用invokeAndHandle这一步才是真正的执行入口。invokeAndHandle内部先通过getMethodArgumentValues解析出所有方法参数。这个方法会遍历每个参数的MethodParameter交给参数解析器组合去解析。如果某个参数解析不了会抛出IllegalStateException提示找不到合适的解析器。这个异常在开发中偶尔会遇到通常是因为参数上用了框架不认识的注解或者泛型信息丢失导致解析器无法判断类型。参数解析完成后通过反射执行目标方法拿到返回值再交给返回值处理器处理。如果返回值处理器写入了响应体比如ResponseBody会标记请求已处理后续不再走视图渲染。如果返回的是视图名或ModelAndView则封装好返回给DispatcherServlet。4.2 参数解析器的匹配顺序为什么重要参数解析器组合内部维护的是一个有序列表匹配时从头到尾遍历第一个supportsParameter返回 true 的就胜出。这个顺序是框架精心设计的不能随意打乱。举个例子RequestParam和RequestBody都能处理String类型的参数但它们的语义完全不同。如果RequestBody的解析器排在前面那一个标注了RequestParam的 String 参数可能被错误地当成请求体解析。框架通过精确的supportsParameter判断检查参数上是否有对应注解来避免这种冲突但如果你自定义解析器时判断条件写得太宽泛就可能插队导致原有逻辑失效。我踩过的一个坑是自定义了一个解析器supportsParameter里只判断了参数类型是User对象就返回 true结果把ModelAttribute绑定的 User 参数也拦截了导致数据绑定失效。后来改成同时判断参数上是否有我的自定义注解问题才解决。这个教训说明自定义解析器的判断条件必须足够精确宁可漏判也不能误判。4.3 返回值处理器如何决定响应形态返回值处理器决定了方法返回之后响应长什么样。常见的几种情况返回String且方法所在类有ResponseBody由RequestResponseBodyMethodProcessor处理字符串直接写入响应体。返回String且无ResponseBody由ViewNameMethodReturnValueHandler处理字符串被当作视图名走视图解析。返回ModelAndView由ModelAndViewMethodReturnValueHandler处理直接返回给DispatcherServlet。返回ResponseEntity由HttpEntityMethodProcessor处理可以精确控制状态码和响应头。返回void由ModelAndViewMethodReturnValueHandler处理返回 null表示由方法自己写响应。这些处理器的匹配同样是有序的ResponseBody相关的处理器优先级很高因为它一旦匹配就意味着响应已经写完不需要再考虑视图。理解这个优先级对排查为什么我的方法返回了视图名却直接输出了字符串这类问题很关键。5. 自定义 HandlerAdapter 的实战与避坑5.1 什么场景下真的需要自定义适配器大部分业务开发根本用不到自定义HandlerAdapter因为注解驱动的适配器已经覆盖了绝大多数需求。真正需要自定义的场景通常有这么几类一是你要接入一种全新的 handler 类型比如公司内部有一套自研的 RPC 框架想把它的服务方法直接暴露成 HTTP 端点。二是你要对现有 handler 的执行过程做统一增强比如在方法调用前后加埋点、加权限校验、加限流。三是你在做框架层面的二次开发需要适配特定的业务对象。需要强调的是如果你的需求只是在 Controller 方法执行前后做点事那用 AOP 或者HandlerInterceptor就够了没必要动适配器。适配器是更底层的扩展点改它的影响面更大风险也更高。5.2 自定义适配器的完整实现步骤假设我们要实现一个适配器用来处理自定义的TaskHandler类型。步骤大致如下第一步定义 handler 接口public interface TaskHandler { Object executeTask(MapString, Object params); }第二步实现适配器public class TaskHandlerAdapter implements HandlerAdapter { Override public boolean supports(Object handler) { return handler instanceof TaskHandler; } Override public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { TaskHandler taskHandler (TaskHandler) handler; MapString, Object params extractParams(request); Object result taskHandler.executeTask(params); writeResponse(response, result); return null; } Override public long getLastModified(HttpServletRequest request, Object handler) { return -1; } }第三步注册到容器。可以通过WebMvcConfigurer的extendHandlerAdapters方法或者直接声明成 Bean。注意注册顺序自定义适配器最好放在默认适配器之后避免抢占。第四步配套实现HandlerMapping让映射阶段能返回TaskHandler类型的 handler。这一步经常被忽略很多人只写了适配器却发现请求根本进不来就是因为映射阶段没有产出对应的 handler。5.3 注册顺序引发的连锁反应前面提过DispatcherServlet遍历适配器列表时是按顺序取第一个匹配的。默认适配器的注册顺序是RequestMappingHandlerAdapter、HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter。如果你通过extendHandlerAdapters添加自定义适配器它会被追加到列表末尾。但如果你是通过Bean直接声明顺序就取决于 Bean 的加载顺序可能排到前面去。一旦自定义适配器的supports判断不够精确就会拦截本该由默认适配器处理的请求。我建议的做法是自定义适配器的supports方法只认自己定义的类型绝不使用宽泛判断同时尽量通过extendHandlerAdapters注册以保证顺序可控。还有一个隐蔽的坑如果你自定义的适配器supports返回 true 但handle方法抛异常DispatcherServlet不会自动回退到其他适配器重试而是直接把异常交给异常解析器。所以supports的判断必须准确不能先揽下来再说。5.4 调试适配器问题的几个实用手段排查适配器相关问题时我常用的几个手段在DispatcherServlet的getHandlerAdapter方法上打断点看请求最终选中了哪个适配器。如果发现选中的不是预期的那个就去看各个适配器的supports返回值。打开org.springframework.web包的 DEBUG 日志框架会输出映射查找和适配器选择的详细信息。这个日志在排查 404 和 405 问题时特别有用。如果怀疑是自定义适配器干扰了默认逻辑可以临时把自定义适配器注释掉看问题是否消失。这是最快的二分定位法。对于参数绑定失败的问题重点看参数解析器的匹配过程。可以在HandlerMethodArgumentResolverComposite.getArgumentResolver方法上打断点观察每个参数最终被哪个解析器处理。6. 从适配器设计看框架扩展点的取舍6.1 为什么是适配器模式而不是继承Spring MVC 没有让DispatcherServlet直接继承各种 handler 的处理逻辑也没有让所有 handler 实现同一个接口而是引入了HandlerAdapter这层间接。这个选择背后有很实际的考量。如果让所有 handler 实现同一个接口那注解方法就没法直接用了因为注解方法的方法签名是任意的不可能统一。如果让DispatcherServlet内部用 if-else 判断 handler 类型再分别处理那每新增一种 handler 类型都要改DispatcherServlet违反开闭原则。适配器模式把判断类型和执行逻辑都下放到了适配器实现里DispatcherServlet只依赖HandlerAdapter这个抽象。新增 handler 类型时只需要新增一个适配器DispatcherServlet完全不用动。这就是为什么 Spring 能不断引入新的编程模型注解、函数式、响应式而核心调度逻辑保持稳定的原因。6.2 适配器与映射器的职责边界初学者容易把HandlerMapping和HandlerAdapter的职责搞混。简单说HandlerMapping负责找到谁来处理HandlerAdapter负责怎么让它处理。HandlerMapping的产出是HandlerExecutionChain里面包含 handler 对象和拦截器列表。它不关心 handler 是什么类型也不关心怎么调用。HandlerAdapter拿到 handler 后才去判断类型、解析参数、执行方法。这个边界划分的好处是映射逻辑和执行逻辑可以独立演化。比如你想换一套映射规则只需要换HandlerMapping适配器不用动想换执行方式只需要换适配器映射不用动。这种解耦在框架层面是标配但在业务代码里经常被忽视值得借鉴。6.3 自定义扩展点的选择建议如果你的需求是改变请求如何被路由到 handler扩展HandlerMapping。如果是改变 handler 如何被执行扩展HandlerAdapter。如果是在 handler 执行前后插入逻辑优先考虑HandlerInterceptor。如果是改变参数如何绑定扩展HandlerMethodArgumentResolver。如果是改变返回值如何处理扩展HandlerMethodReturnValueHandler。这几个扩展点的侵入性是从高到低排列的HandlerAdapter和HandlerMapping影响面最大参数解析器和返回值处理器次之拦截器最小。选择扩展点时能用低侵入方案解决的就不要动高侵入的。我见过有人为了加个请求日志直接自定义了HandlerAdapter结果把整个参数绑定链路都搞乱了得不偿失。7. 读源码过程中我总结的几个判断技巧读HandlerAdapter相关源码时有几个判断技巧能帮你快速理清逻辑。第一个技巧先看 supports再看 handle。supports决定了适配器的适用范围看懂它基本就知道这个适配器是干什么的了。handle是具体实现细节多但逻辑主线清晰。第二个技巧抓住组合对象。RequestMappingHandlerAdapter本身代码不多复杂的是它持有的那几个组合对象。读的时候先把组合对象的职责搞清楚再回头看handle方法会发现每一步都能对应上。第三个技巧关注 null 返回值的语义。handle返回 null 不是异常情况而是无需视图渲染的正常信号。理解这一点能避免很多为什么这里返回 null的困惑。第四个技巧顺着异常往上追。适配器抛出的异常最终会到HandlerExceptionResolver如果你在排查异常处理问题从适配器的handle方法往上追能清楚看到异常是在哪个环节产生的。第五个技巧对照官方文档的流程图。Spring 官方文档里有一张请求处理的流程图把DispatcherServlet、HandlerMapping、HandlerAdapter的关系画得很清楚。读源码前先看一遍图读的时候对照着看效率会高很多。最后分享一个我自己的习惯读这类框架源码时我会在关键方法上打断点然后启动一个最小化的 Demo 项目发一个请求单步跟一遍完整流程。纸上读十遍不如实际跟一遍尤其是参数解析和返回值处理这种分支很多的逻辑跟一遍断点比看半天代码管用得多。适配器这块的逻辑分支不算特别多跟两三个不同类型的请求普通RequestMapping、ResponseBody、HttpRequestHandler基本就能把主链路摸清楚了。