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里维护的是一个List<HandlerAdapter>,遍历顺序就是 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 接口方法总览与调用时机对照
把三个方法放在一起看,能更清楚地理解它们各自的定位:
| 方法名 | 入参 | 返回值 | 调用时机 | 核心职责 |
|---|---|---|---|---|
| supports | Object handler | boolean | 映射查找完成后,选择适配器时 | 判断能否处理该 handler |
| handle | request, response, handler | ModelAndView | 适配器选定后,执行 handler 时 | 参数绑定、方法调用、返回值处理 |
| getLastModified | request, handler | long | handle 执行前,缓存判断时 | 提供 last-modified 时间戳 |
这三个方法的调用顺序是固定的:先supports筛选,再getLastModified判断缓存,最后handle执行。DispatcherServlet的doDispatch方法里,这段逻辑写得非常清晰,建议对照源码读一遍,比看任何二手资料都管用。
3. 内置适配器实现类:谁在适配什么样的 handler
3.1 RequestMappingHandlerAdapter:注解驱动的主力
这是日常开发中出场率最高的适配器,没有之一。你写的所有@Controller+@RequestMapping方法,最终都是被它执行的。它的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 HttpRequestHandler,handle方法直接调用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 HandlerFunction,handle方法会调用函数并处理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(Map<String, 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; Map<String, 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),基本就能把主链路摸清楚了。