ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringMVC请求处理流程源码拆解:HandlerMapping与HandlerAdapter深度解析

SpringMVC请求处理流程源码拆解:HandlerMapping与HandlerAdapter深度解析 写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和HandlerAdapterDispatcherServlet不需要任何改动。这正是策略模式和适配器模式在框架设计上的典型组合。理解了这个动机再看后面的源码就不会觉得抽象。2. HandlerMapping把URL变成可执行的handler2.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 MapString, 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(ListHandlerAdapter 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定义了三个默认方法preHandlehandler执行前调用返回false可阻断请求。postHandlehandler执行后、视图渲染前调用可以修改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直接返回trueDispatcherServlet会退出等业务线程完成异步结果时才会再次触发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;charsetUTF-8或者直接替换消息转换器。排查乱码时先看响应头里的Content-Type带不带charsetutf-8这是最快区分问题方向的手段。5.4 Ambiguous mapping多方法映射冲突启动时如果日志抛出“Ambiguous mapping”异常说明两个方法映射到了同一个请求条件上。最常见于同名重载方法一个类里两个方法都标了GetMapping(“/same”)Spring无法判断到底该用哪个直接拒绝启动。排查方法是找异常信息里包含的两个类的类名和方法名看它们是否确实共享了同一个路径和请求方法组合。如果一个是get一个是post框架能区分不会报错但两个get必然冲突。处理方案就是改路径、改请求方式或者合并成一个方法。我遇到过一个隐蔽场景Controller方法上用了GetMapping类上没有路径而另一个Controller的方法路径恰好在同一根路径下两个都有通配符最终也被判定为冲突。这种通配符重叠问题比完全相同的路径更难发现需要把RequestMapping条件整体打印出来逐一对比。这些年排查下来我最大的体会是真正吃透SpringMVC执行流程不需要把源码逐行背下来但一定要把组件职责、调用顺序和接口约定这三件事记牢。拿到一个诡异问题先确认是卡在HandlerMapping的查找阶段还是卡的HandlerAdapter的执行阶段方向对了问题就解决了一半。我对新人的建议是遇到请求问题先别急着换框架加依赖回到doDispatch里给getHandler和handle各打一个条件断点看看mappedHandler到底找到没找到、适配器到底是哪个实现类事实往往比你推测的简单得多。
返回列表