ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring MVC源码解析:从DispatcherServlet到扩展点实战

Spring MVC源码解析:从DispatcherServlet到扩展点实战 先聊个实在的Spring MVC源码这东西你打开IDE就能看但多数人翻两页就劝退了。原因很简单类太多、抽象层太厚、回调太绕光DispatcherServlet一个类就一千多行硬啃跟看天书没区别。但这套框架又是Java后端绕不开的骨架面试要问、线上问题排查要翻、想自己写点脚手架也得抄它的思路。我这篇就把Spring MVC的源码拆开揉碎从核心接口到设计模式再拿几个能直接落地的经典案例串一遍目标只有一个让你读完能自己顺着doDispatch把一次请求的完整旅程讲清楚顺手还能在业务里用上它的扩展点。我自己大概是从2016年开始系统啃Spring MVC源码的期间踩过不少坑也靠它解决过线上诡异的404和参数丢失问题。这篇的内容不是源码注释的搬运而是我结合实际排查经验梳理出来的“阅读地图”——哪些类必须看、哪些可以略过、每个核心接口到底在解决什么问题、框架为什么这么设计。如果你正准备啃源码但找不到抓手或者已经在看但被各种Handler、Adapter绕晕这篇应该能帮你省不少时间。1. 先把全景图装进脑子一次请求从进入到返回的完整链路要读懂Spring MVC源码第一件事不是去翻类而是先建立一条主线一次HTTP请求进来框架到底做了哪几件事。我习惯把这套流程归纳成“四步走”定位处理器、适配调用、拦截增强、结果渲染。所有核心类和接口都是围绕这四个环节长出来的。1.1 DispatcherServlet是整个框架的中枢神经DispatcherServlet继承自FrameworkServlet再往上追是HttpServletBean最终落到HttpServlet。它承担的角色你可以理解成公司前台所有请求先到它手里它不干活但知道谁适合干这个活然后统一调度。关键代码在doDispatch方法里我把核心流程提炼成以下几步检查是不是multipart请求文件上传是的话先用MultipartResolver包装一下。调用getHandler(request)拿到执行链HandlerExecutionChain。这一步是通过HandlerMapping实现的如果找不到对应的Handler直接抛404。拿到HandlerAdapter这是适配器的关键一步——不同种类的Handler需要不同的适配器去调用。依次执行拦截器的preHandle如果返回false直接短路返回。真正调用handler.handle()执行业务逻辑。执行拦截器的postHandle此时视图还没渲染。处理返回结果——解析ModelAndView或者直接写响应体ResponseBody场景。最后触发afterCompletion用于资源清理。这个顺序我建议你背下来后面所有源码分析都是在这条主线上开枝散叶。1.2 从doDispatch源码看框架的容错与兜底设计doDispatch这段代码我读了很多遍每次都有新收获。它最值得学习的地方不是流程本身而是异常处理的层次感。整个方法被包裹在try-catch里捕获了Exception但注意它没有直接处理而是往上抛由processDispatchResult里的HandlerExceptionResolver统一解决。这里有个细节如果processDispatchResult内部又抛了异常doDispatch里还有个catch (Exception ex)配合dispatchException判断要不要走triggerAfterCompletion。这种“异常也分阶段处理”的设计让业务异常和框架异常各得其所不会互相污染。我当年读到这里拍了下大腿——这就是教科书级的容错分层我们平时写业务代码往往一个try-catch从头包到尾其实完全可以把“参数校验异常”“业务异常”“系统异常”分别处理各归各路。2. 核心接口逐个拆解每个接口存在的理由都藏着设计智慧Spring MVC的扩展性本质上是接口的扩展性。我梳理了五个最重要的核心接口你可以照着这个清单去读源码比漫无目的翻类效率高得多。2.1 HandlerMapping请求与处理器的路由表HandlerMapping的核心职责就一句话根据请求找到对应的Handler。它只有一个核心方法Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;但它的实现类非常多各有各的用途。我给你整理了一个表格方便对照记忆实现类核心用途适用场景RequestMappingHandlerMapping基于RequestMapping注解的路由最常用注解式ControllerSimpleUrlHandlerMapping基于URL模式配置的显式映射XML配置时代、静态资源处理BeanNameUrlHandlerMapping把Bean的名字当成URL路径极简配置场景RouterFunctionMapping函数式路由配合RouterFunctionWebFlux风格的函数式编程我最常用的是RequestMappingHandlerMapping它在初始化时会扫描容器中所有Controller和RequestMapping注解把方法和URL的对应关系缓存到MappingRegistry里。这里面有几个设计细节值得注意RequestMappingHandlerMapping内部维护了一个MappingRegistry它包含两个核心MappathLookup路径到Mapping的映射和registryMapping到HandlerMethod的映射。每次请求进来先按路径匹配到RequestMappingInfo再从registry里拿到具体的HandlerMethod。这种两层索引的设计保证了路由查找的高效性——不会为了找一个Handler去遍历所有Controller方法。2.2 HandlerAdapter适配器模式的教科书实现框架拿到Handler之后接下来问题来了Handler的种类千差万别有注解方法的HandlerMethod有实现Controller接口的传统Bean有HttpRequestHandler调用方式完全不同。如果让DispatcherServlet直接判断类型去调用这个类会膨胀到无法维护。HandlerAdapter就是为了解决这个问题诞生的它的接口定义如下public interface HandlerAdapter { boolean supports(Object handler); Nullable ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; }对应的核心实现类分别是RequestMappingHandlerAdapter支持HandlerMethod也就是RequestMapping注解方法。内部真正干活的是ServletInvocableHandlerMethod通过HandlerMethodArgumentResolver解析参数、HandlerMethodReturnValueHandler处理返回值。SimpleControllerHandlerAdapter支持实现了Controller接口的Bean调用其handleRequest方法。HttpRequestHandlerAdapter支持HttpRequestHandler常用于静态资源处理和特殊协议处理。DispatcherServlet在getHandlerAdapter方法中遍历所有注册的HandlerAdapter用supports方法判断哪个适配器能处理当前Handler。这个过程就是策略模式的体现——调用方不关心具体实现只关心能不能用。2.3 HandlerInterceptorAOP思想在Web层的落地HandlerInterceptor是所有切面需求的答案登录校验、日志记录、权限控制、接口幂等、耗时监控全部可以塞进这里。它的三个方法分别对应请求生命周期的三个节点public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable ModelAndView modelAndView) throws Exception { } default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable Exception ex) throws Exception { } }执行顺序是preHandle按注册顺序正向执行postHandle和afterCompletion按注册顺序反向执行。当某个preHandle返回false时框架会触发已执行过preHandle的拦截器的afterCompletion但后续拦截器不再执行。这个逻辑在DispatcherServlet的applyPreHandle和triggerAfterCompletion方法里体现得很清晰。我在实际项目中踩过一个坑为了做统一鉴权在拦截器里往ModelAndView塞公共数据结果发现ResponseBody的接口根本走不到postHandle——因为响应体已经写完了。后来查源码才知道RequestResponseBodyMethodProcessor处理返回值时直接写入了响应流根本不会返回ModelAndView。这个教训让我养成了习惯需要公共数据时优先考虑HandlerMethodArgumentResolver或ResponseBodyAdvice而不是拦截器。2.4 HandlerMethodArgumentResolver参数绑定的幕后推手你用RequestParam、PathVariable、RequestBody、ModelAttribute时有没有想过这些参数是怎么被解析出来的答案就在HandlerMethodArgumentResolver体系里。接口定义很简单public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); Nullable Object resolveArgument(MethodParameter parameter, Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, Nullable WebDataBinderFactory binderFactory) throws Exception; }supportsParameter判断是否支持某个参数resolveArgument负责真正解析出参数值。RequestMappingHandlerAdapter里维护了一个argumentResolvers列表执行时按顺序调用supportsParameter谁支持谁上。这个接口是扩展性最强的点之一。举个例子你想在Controller方法里直接拿到当前登录用户GetMapping(/profile) public Result getProfile(CurrentUser user) { // 直接用不用再手动从Session或Token里解析了 }实现方式就是自定义一个HandlerMethodArgumentResolver在supportsParameter里判断参数类型是不是CurrentUser然后在resolveArgument里从Token解析出用户信息并返回。这个操作我后面会给出完整案例。2.5 HandlerExceptionResolver异常处理的统一出口框架的异常处理也是策略化的核心接口是HandlerExceptionResolverpublic interface HandlerExceptionResolver { Nullable ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Nullable Object handler, Exception ex); }Spring MVC内置了几个关键实现ExceptionHandlerExceptionResolver最常用解析ExceptionHandler注解方法也是RestControllerAdvice机制的基础。ResponseStatusExceptionResolver处理ResponseStatus注解标注的异常。DefaultHandlerExceptionResolver处理Spring MVC内置的标准异常如NoHandlerFoundException、TypeMismatchException转换成对应的HTTP状态码。processDispatchResult方法的逻辑是先正常处理请求如果有异常抛出来就遍历这些HandlerExceptionResolver按序调用直到某个解析器返回了非空的ModelAndView。这个设计非常优雅你完全可以通过自定义HandlerExceptionResolver来实现自己的异常处理链路或者用ExceptionHandler按异常类型精确处理。3. 源码里的设计模式这些才是Spring MVC能被广泛复用的根基很多人在简历里写“熟悉设计模式”但真要说出框架里的具体应用场景就卡壳了。Spring MVC源码本身就是活的设计模式教材每一个核心接口背后都有明确的模式支撑。搞清楚这些你面试讲源码时才有底气。3.1 前端控制器模式所有请求的统一入口DispatcherServlet是典型的前端控制器模式Front Controller Pattern实现。所有请求先汇聚到这个中央处理器由它负责调度。这样做的好处是请求的公共逻辑编码设置、切面处理、异常捕获可以统一收口业务处理逻辑则可以独立演化。传统Servlet开发中每个Servlet各管各的登录请求写一个LoginServlet注册写一个RegisterServlet每个Servlet里都要写编码设置、权限校验、异常处理重复代码满天飞。前端控制器模式把这个局面彻底扭转了这也是我喜欢说的“收口思维”——把变的东西收进去把不变的东西暴露出来。3.2 策略模式框架如何优雅地替换核心组件策略模式在Spring MVC里用得最为密集。核心思想是定义一组算法将每个算法封装起来使它们可以互换。框架里最典型的例子就是HandlerMapping和HandlerAdapter。DispatcherServlet初始化时会从容器中获取所有HandlerMapping实例放入handlerMappings列表请求进来时逐个调用getHandler谁返回非空就采用谁。你完全可以在不改动框架代码的情况下往容器里塞一个自定义的HandlerMapping实现一套完全不同的路由逻辑。这种可插拔设计就是策略模式的价值所在。再看LocaleResolver、ThemeResolver、ViewResolver这套Resolver家族都是策略模式的产物。框架通过接口定义行为通过容器装配具体实现调用方只依赖抽象接口。这也是为什么Spring MVC能存活这么多年、从XML配置平滑过渡到注解配置、再到Spring Boot的自动配置核心组件却几乎不用动。3.3 适配器模式统一不同Handler的调用差异HandlerAdapter是适配器模式最典型的应用。为什么需要适配器因为要适配的Handler类型太多了。HandlerMethod背后是一个带各种注解的Controller方法需要解析参数、处理返回值Controller接口是传统的handleRequest方法HttpRequestHandler是面向流式处理的接口。它们之间的调用方式完全不一样如果不加适配层DispatcherServlet就得写一堆if-else去做类型判断和switch调用。有了HandlerAdapter之后DispatcherServlet只认识一个接口handle(request, response, handler)。具体怎么调用、怎么解析参数、怎么处理返回值全部封装到适配器内部。这就是适配器模式的核心让不兼容的接口通过适配层协同工作让调用方依赖统一抽象而不是具体实现。3.4 模板方法模式骨架流程谁来定义模板方法模式在老版本的FrameworkServlet和HttpServletBean里体现得非常充分。以FrameworkServlet的service方法为例protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long startTime System.currentTimeMillis(); Throwable failureCause null; // 关键扩展点 initContextHolders(request, response); try { doService(request, response); } catch (Exception ex) { failureCause ex; throw ex; } finally { resetContextHolders(request, response); if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) null) { // 发布请求处理完成事件 } } }其中doService是抽象方法由DispatcherServlet实现。父类定义了请求处理的整体流程记录开始时间、初始化上下文、调用子类实现、清理上下文子类只需要填充具体的业务步骤。这种骨架式设计的精髓在于公共逻辑统一管理变化逻辑下沉到子类。3.5 责任链模式与观察者模式的精妙配合责任链模式在拦截器执行链中体现得最为明显。多个HandlerInterceptor组成一条链路每个节点都有机会处理请求也可以中断链路返回false。这种模式的好处是灵活——可以动态决定链路中要加入哪些拦截器每个拦截器只关心自己的职责。观察者模式在Spring MVC中的典型应用是ApplicationEvent机制。FrameworkServlet在每个请求处理完会发布ServletRequestHandledEvent各种ApplicationListener可以监听这个事件做监控、日志、埋点。它是Spring事件体系的延伸体现了“发布-订阅”的思想让框架的核心流程对外部扩展保持开放。我建议研究源码时把“模式识别”当成一道练习题每看到一个接口就想想它属于哪种设计模式为什么框架选这种模式。做多了之后你自己写业务代码的思路会开阔很多。4. 基于源码扩展点的经典案例把理论变成能落地的代码源码读得再透落不了地就是空中楼阁。我挑了三个基于Spring MVC扩展点的实战案例难度从低到高排列每一个我在真实项目里都用过代码可以直接复用到自己的工程里。4.1 案例一自定义HandlerMethodArgumentResolver注入当前登录用户这个案例最实用也是新手最容易上手的扩展点。场景是接口需要一个UserInfo对象不想在每个方法里手动从Token解析。实现方案分三步走。第一步定义用户类public class UserInfo { private Long id; private String name; private String role; // getter/setter省略 }第二步实现HandlerMethodArgumentResolverComponent public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { private final UserTokenService userTokenService; public CurrentUserArgumentResolver(UserTokenService userTokenService) { this.userTokenService userTokenService; } Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().equals(UserInfo.class) parameter.hasParameterAnnotation(CurrentUser.class); } Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { HttpServletRequest request webRequest.getNativeRequest(HttpServletRequest.class); String token request.getHeader(Authorization); return userTokenService.parse(token); } }supportsParameter判断参数类型是UserInfo且加了CurrentUser注解时才处理。resolveArgument里从请求头取出Token调用解析服务返回用户对象。第三步注册到WebMvcConfigurerConfiguration public class WebConfig implements WebMvcConfigurer { private final CurrentUserArgumentResolver currentUserArgumentResolver; public WebConfig(CurrentUserArgumentResolver currentUserArgumentResolver) { this.currentUserArgumentResolver currentUserArgumentResolver; } Override public void addArgumentResolvers(ListHandlerMethodArgumentResolver resolvers) { resolvers.add(currentUserArgumentResolver); } }这样Controller就能直接收参数了GetMapping(/profile) public Result? profile(CurrentUser UserInfo user) { return Result.ok(user); }说个心得解析器一定记得判断注解如果不加hasParameterAnnotation判断所有UserInfo类型的参数都会被这个解析器接管一旦出现其他含义的UserInfo参数就会出问题。4.2 案例二自定义HandlerInterceptor实现接口幂等这个案例适合写操作频繁、防止重复提交的场景。思路是在拦截器里对指定接口做幂等校验请求进来时检查请求头里的幂等Key如果已经处理过就拦截。实现HandlerInterceptorComponent public class IdempotentInterceptor implements HandlerInterceptor { private final RedisTemplateString, Object redisTemplate; public IdempotentInterceptor(RedisTemplateString, Object redisTemplate) { this.redisTemplate redisTemplate; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod handlerMethod)) { return true; } Idempotent annotation handlerMethod.getMethodAnnotation(Idempotent.class); if (annotation null) { return true; } String token request.getHeader(Idempotent-Key); if (StringUtils.isBlank(token)) { throw new BizException(缺少幂等Key); } String lockKey idempotent: token; Boolean first redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(30)); if (!Boolean.TRUE.equals(first)) { response.setStatus(HttpStatus.OK.value()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\: 429, \msg\: \请求处理中或已处理过\}); return false; } return true; } }这里用了setIfAbsent加分步实现锁同一个幂等Key在30秒内只允许第一个请求通过。拦截器里通过handlerMethod.getMethodAnnotation精确判断哪些方法需要幂等保护用注释驱动不会误伤其他接口。注意一点HandlerInterceptor里的postHandle和afterCompletion的执行顺序容易搞混。postHandle在DispatcherServlet调用完Handler之后、视图渲染之前执行afterCompletion在请求完全结束后执行。如果要在请求结束后删除幂等Key比如处理失败允许重试在afterCompletion里判断是否有异常再删。4.3 案例三利用HandlerMapping做灰度发布路由这个案例相对进阶适合有路由需求的场景比如按用户ID灰度到新版本接口。核心思路是自定义一个HandlerMapping在getHandler里按请求头和用户信息路由到不同的HandlerMethod。实现方案是继承RequestMappingHandlerMapping重写lookupHandlerMethod方法public class GrayRequestMappingHandlerMapping extends RequestMappingHandlerMapping { Override protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { HandlerMethod method super.lookupHandlerMethod(lookupPath, request); if (method null) { return null; } String userId request.getHeader(X-User-Id); // 灰度用户走v2版本Controller if (userId ! null grayUserService.isGray(userId)) { // 构造v2版本的查找路径 String grayLookupPath /v2 lookupPath; HandlerMethod grayMethod super.lookupHandlerMethod(grayLookupPath, request); if (grayMethod ! null) { return grayMethod; } } return method; } }然后在配置类里替换默认的映射器Configuration public class GrayRoutingConfig implements WebMvcConfigurer { Override public void configurePathMatch(PathMatchConfigurer configurer) { // 自定义HandlerMapping需要手动注册到容器 } }这个方案在真实项目中要评估复杂度灰度路由切面比较重建议先考虑网关层实现。但如果你不想引入额外的网关组件直接用HandlerMapping扩展点也能做到动态路由。核心思路是理解框架的路由查找机制在这个环节做手脚。5. 源码阅读的实用方法论与踩坑记录读源码最大的痛点不是代码难而是不知道从哪下手、哪些该看哪些该跳过。我把自己这些年读Spring MVC源码的“笨办法”整理了一下希望帮你少走弯路。5.1 先定目标再读代码带着问题去翻类直接从头读DispatcherServlet是效率最低的方式因为它的依赖太多了。我更推荐“问题驱动式”阅读比如你想搞懂“为什么RequestBody能拿到JSON”那先写个Demo打个断点看请求进了RequestResponseBodyMethodProcessor之后是如何调HttpMessageConverter的你想搞懂“多个拦截器执行顺序”那就写三个拦截器分别打印日志观察输出顺序。从“现象”倒推“源码”比从“类”正推“流程”快得多。我当年排查一个诡异问题时接口在某些情况下返回了空对象怎么都找不到原因。后来打点发现是自定义的HandlerMethodArgumentResolver和框架自带的ServletModelAttributeMethodProcessor在争夺解析权supportsParameter判断条件写得太宽把本该由框架处理的参数也截胡了。这个坑如果不打断点看执行栈靠肉眼排查很难定位。5.2 常用调试点位断点打在哪儿最有效我给大家整理了一份调试点位清单都是源码阅读中性价比最高的断点位置断点位置类名/方法名你能看到什么请求入口DispatcherServlet.doDispatch完整请求生命周期路由查找AbstractHandlerMapping.getHandler哪个HandlerMapping命中、拦截器有哪些参数解析InvocableHandlerMethod.getMethodArgumentValues参数解析器逐个匹配过程方法调用ServletInvocableHandlerMethod.invokeAndHandle反射调用Controller方法、返回值处理异常处理DispatcherServlet.processDispatchResult异常分发到哪个ExceptionResolver视图渲染DispatcherServlet.renderViewResolver的匹配过程每个断点都配合“表达式计算”观察关键变量的值比如在doDispatch里看mappedHandler是怎么被赋值的在getMethodArgumentValues里看resolvers列表里到底有哪些解析器以及它们的执行顺序。5.3 几个容易踩的细节坑细节坑一ResponseBody和ResponseEntity的返回值处理路径不一样。前者走RequestResponseBodyMethodProcessor直接写响应体后者需要HttpEntityMethodProcessor参与。如果自定义了返回值处理器要注意注册顺序否则可能覆盖框架默认行为。细节坑二拦截器里的postHandle方法在ResponseBody场景下拿不到ModelAndView因为响应体已经写出。如果你需要往响应头里加东西考虑ResponseBodyAdvice或者直接在Controller里用HttpServletResponse操作。细节坑三异步请求Callable、DeferredResult、WebAsyncTask的拦截器执行流程和同步请求不一样。preHandle执行完后会先把线程归还容器等异步结果产生后再重新进入拦截器链路的后续环节。具体实现可以看WebAsyncManager里的startCallableProcessing方法这块内容很容易被忽略但市面上有大量此类面试题。写在最后Spring MVC源码读过和没读过的人看待同一个报错的视角完全不同。没读过的人看到404只会查URL对不对读过之后你会下意识打开日志看DispatcherServlet的noHandlerFound路径、检查HandlerMapping是否注入了自定义实现、甚至会怀疑是不是拦截器短路了。这种“问题定位的颗粒度差异”就是源码学习的复利效应。关于怎么持续深入我再给两条自己的经验第一读源码不要追求“全”先把DispatcherServlet这条主干流程吃透然后按业务需求去扩展——需要处理文件上传就看MultipartResolver需要做参数加密就看HandlerMethodArgumentResolver遇到问题再精准切入。第二一定要配合实战自己写定时器任务在本地打印执行链路、打断点观察变量状态远比把代码从头到尾看一遍有效。还有个小技巧分享一下遇到看不懂的调用链直接在IDE里右键“Find Usages”把每个方法的调用方理一遍画个草图就是现成的类图。这个习惯帮我节省了大量时间等这套方法用得顺了你再去读MyBatis、读Netty的源码会发现套路都是通的。
返回列表