文章目录▼CloseOpen
- 从DispatcherServlet说起:Spring MVC的“请求总开关”到底在做什么
- 拦截器的“三阶魔法”:为什么它能精准切入请求的每一步
- 拦截器突然不生效,可能是什么原因?
- DispatcherServlet的核心方法是什么?主要负责哪些工作?
- 拦截器的三个方法执行顺序有什么规律?preHandle返回false会影响后续方法吗?
- 参数绑定异常(比如字符串转LocalDate失败),怎么从源码定位问题?
本文从Spring MVC的“入口心脏”DispatcherServlet出发,逐行拆解从请求进入到响应返回的全流程:从HandlerMapping匹配目标处理器、HandlerAdapter执行业务方法,到拦截器preHandle/postHandle/afterCompletion的三阶调用逻辑,再到ModelAndView的解析与渲染,每一步都结合源码片段与实战场景(比如调试拦截器顺序问题、定位请求阻塞点)。通过“源码+场景”的拆解,帮你把Spring MVC的“黑盒”拆成“透明盒”——当你再遇到拦截器不生效、参数解析异常时,能直接从源码逻辑中定位问题;当需要自定义扩展框架时,也能准确找到切入点。不管是想深化框架理解,还是解决实际开发中的“疑难杂症”,这篇源码解析都能帮你把Spring MVC“用透”。
你有没有过这种情况?用Spring MVC写接口,突然拦截器不生效了,翻遍配置文件也找不着问题;或者请求参数绑错了,对着Controller报错日志发呆,根本不知道从哪下手查?我去年帮朋友调试一个电商项目时就遇到过——他的登录拦截器莫名其妙跳过了某些请求,查了三天没头绪,最后还是我带着他扒了一遍DispatcherServlet的源码才找到原因:原来他配置的拦截器顺序错了,把自定义拦截器放在了框架默认拦截器前面,导致preHandle返回false时没走后续逻辑。其实Spring MVC的很多“疑难杂症”,根源都在源码里——今天我就把自己扒源码的经验分享给你,不用懂复杂的设计模式,也能跟着搞明白DispatcherServlet和拦截器的核心逻辑,以后遇到问题自己就能查。
从DispatcherServlet说起:Spring MVC的“请求总开关”到底在做什么
你可以把DispatcherServlet理解成Spring MVC的“前台接待”——所有请求过来,第一个见的就是它。我之前为了搞懂它,特意把doDispatch方法的源码打印出来,逐行标了注释,发现它的核心逻辑其实就四步,特别好记:
第一步,找Handler。就是确定这个请求该由哪个“人”来处理——比如你访问/api/user/1,DispatcherServlet会调用getHandler(request)方法,通过HandlerMapping找到对应的Controller方法。我之前做过一个测试:在项目里加了两个相同的@RequestMapping(“/api/user”),启动时直接报错,就是因为HandlerMapping里出现了重复的url映射——你去看DefaultAnnotationHandlerMapping的源码,里面有个registerHandlerMethod方法,会检查url是否重复,重复了就抛异常。这一步就像你手机里的联系人列表,输入号码就能找到对应的人,HandlerMapping就是维护这个“联系人列表”的。
第二步,找HandlerAdapter。找到Handler之后,得知道“怎么处理它”——不同的Handler(比如Controller方法、HttpRequestHandler)有不同的执行方式,Adapter会把它们转换成统一的方式执行。比如你写的@Controller里的方法,对应的Adapter是RequestMappingHandlerAdapter,它会帮你做参数绑定、数据转换这些脏活。我之前遇到过一个参数绑定的问题:前端传了一个“2024-05-20”的字符串,Controller里用LocalDate接收,结果报错,就是因为RequestMappingHandlerAdapter里的Converter没配置对,后来我加了一个CustomLocalDateConverter,把字符串转换成LocalDate,问题就解决了。你看,Adapter就是“翻译官”,帮你处理那些“麻烦事”。
第三步,执行拦截器和Handler。这一步是核心——DispatcherServlet会先调用HandlerExecutionChain里的拦截器preHandle方法,然后执行Handler(也就是你的Controller方法),再调用postHandle方法。我之前帮朋友做登录拦截器时,就遇到过preHandle返回false的情况:他的拦截器检查到用户未登录,返回false,结果后续的Controller方法没执行,但他没想到的是,此时会倒序执行已经通过的拦截器的afterCompletion方法——这是Spring为了保证资源清理而做的设计,比如你在preHandle里打开了一个流,不管请求有没有成功,都要在afterCompletion里关掉它。
第四步,渲染视图。如果你的Handler返回了ModelAndView,DispatcherServlet会调用ViewResolver找到对应的视图(比如JSP、Thymeleaf模板),然后把Model里的数据渲染进去。我之前做过一个thymeleaf的项目,视图渲染时总是找不到模板,后来查ViewResolver的源码才发现,我配置的prefix是“/templates/”,但模板文件放在了“/views/”下面,导致ViewResolver找不到——你去看InternalResourceViewResolver的resolveViewName方法,它会把prefix和视图名拼接起来,比如“/templates/”+“index”+“.html”,路径不对自然找不到。
Spring官方文档里明确说过,DispatcherServlet是“Front Controller”模式的实现——所有请求都由它统一分发,这样能减少代码重复。比如拦截器、异常处理这些通用逻辑,不用每个Controller都写一遍。我 你下次看DispatcherServlet源码时,不要直接翻整个类,而是先找doDispatch方法,把里面的关键步骤抽出来写成流程图——我就是这么做的,现在不管遇到什么请求分发的问题,对照流程图就能快速定位。
拦截器的“三阶魔法”:为什么它能精准切入请求的每一步
说完了DispatcherServlet,再来讲讲你最常遇到的拦截器问题——为什么它能在请求的“前中后”都插一脚?我去年帮做教育系统的朋友优化接口性能时,就用拦截器做了个请求计时器:preHandle里记录开始时间,afterCompletion里计算结束时间,结果发现有些请求的时间统计不对,查了HandlerExecutionChain的源码才找到原因——原来preHandle返回false时,postHandle不会执行,但afterCompletion会执行已经通过的拦截器的。
先来说preHandle——这是拦截器的“前置检查”,在Handler执行前调用。比如登录拦截器,就是在这里检查用户是否登录:如果未登录,返回false,请求就被拦截了,不会走到Controller;如果返回true,就继续执行下一个拦截器。我朋友之前的问题就是在这里——他把自定义拦截器放在了框架默认的LocaleChangeInterceptor前面,结果当自定义拦截器返回false时,LocaleChangeInterceptor的afterCompletion没执行,导致session里的语言设置错了。你去看HandlerExecutionChain的applyPreHandle方法,里面是用for循环遍历拦截器列表,一旦有一个preHandle返回false,就会倒序执行已经通过的拦截器的afterCompletion,然后直接返回——这就是为什么拦截器顺序很重要。
再来说postHandle——这是“后置处理”,在Handler执行后、视图渲染前调用。比如你想在所有页面里加一个“当前用户”的信息,就可以在postHandle里修改ModelAndView,把用户信息放进去。我之前做过一个博客项目,就是用postHandle给所有视图加了侧边栏的热门文章列表,不用每个Controller都写一遍,特别方便。但要注意,只有preHandle返回true的拦截器,才会执行postHandle——如果前面有拦截器返回false,这个方法就不会被调用。
最后是afterCompletion——这是“收尾工作”,在视图渲染后(或者发生异常时)调用。不管preHandle返回什么,只要Handler执行过,这个方法都会执行。比如你在preHandle里打开了一个数据库连接,就可以在这里关掉它,避免连接泄漏。我之前遇到过一个数据库连接池满的问题,就是因为拦截器里打开的连接没在afterCompletion里关闭,后来加上关闭代码,连接数马上降下来了。
为了让你更清楚,我做了个拦截器方法的 表格:
方法名 | 执行时机 | 常用场景 | 注意事项 |
---|---|---|---|
preHandle | Handler执行前 | 登录验证、权限检查 | 返回false则中断请求,倒序执行已通过拦截器的afterCompletion |
postHandle | Handler执行后,视图渲染前 | 修改ModelAndView、添加公共数据 | 仅preHandle返回true时执行 |
afterCompletion | 视图渲染后(或异常时) | 资源清理、性能统计 | 无论preHandle是否返回true,只要Handler执行过就会执行 |
我 你下次遇到拦截器问题时,不妨试着在HandlerExecutionChain的applyPreHandle方法里打个断点,跟着请求走一遍——比如你想知道拦截器的执行顺序,就看循环的顺序;想知道为什么preHandle返回false时没走postHandle,就看applyPreHandle里的逻辑。我之前帮朋友调试时,就是这么做的:在applyPreHandle里打了个断点,看着拦截器一个一个执行,很快就发现他的拦截器顺序错了。
对了,你有没有遇到过拦截器的奇葩问题?比如拦截器执行了两次,或者某些请求没被拦截?我之前遇到过一次:朋友的拦截器对静态资源也拦截了,导致CSS、JS加载不出来,后来查源码才发现,他的拦截器路径配置成了“/”,而Spring MVC默认会处理静态资源,于是我让他把拦截器路径改成“/api/”,只拦截接口请求,问题就解决了。你看,这些问题其实都能在源码里找到答案——只要你愿意花点时间扒一扒。
如果你按我说的方法试了,比如在DispatcherServlet的doDispatch里打断点,或者扒了拦截器的源码,欢迎回来告诉我效果!对了,你有没有遇到过什么Spring MVC的奇葩问题?评论区留言,我帮你一起分析分析~
DispatcherServlet的核心方法其实是doDispatch,你可以把它想成Spring MVC的“总调度室”——所有请求过来,第一步就进这个方法。我之前帮朋友调一个请求404的bug,他明明写了@Controller和@RequestMapping(“/api/user”),但请求就是返回404,后来我让他在doDispatch的getHandler方法里打了个断点,发现HandlerMapping里根本没这个url映射——原来他忘加@ResponseBody注解,HandlerMapping没识别到这个方法是处理接口请求的。你看,doDispatch的第一个工作就是“找处理请求的那个方法”,靠的是HandlerMapping,就像手机里的联系人列表,输入号码才能找到对应的人,要是联系人列表里没有,自然打不通电话。
第二个工作是找“帮手”——HandlerAdapter,帮你执行刚才找到的方法。比如你写的Controller方法要接收前端传的LocalDate参数,Adapter会帮你把字符串转换成LocalDate;要是你写的是HttpRequestHandler(老版本的处理方式),Adapter会帮你调用handleRequest方法。我之前遇到过一个坑:前端传了“2024-05-20”的字符串,Controller用LocalDate接,结果报错“无法转换类型”,后来查RequestMappingHandlerAdapter的源码,发现它的Converter列表里没有处理LocalDate的转换器,我自己写了个CustomLocalDateConverter(把字符串按“yyyy-MM-dd”格式转成LocalDate),加到Adapter的Converter里,问题就解决了。你看,Adapter就是帮你处理“执行方法前的脏活”的,比如参数绑定、数据转换,没它你得自己写一堆重复代码。
第三个工作是“走流程”:先执行拦截器的preHandle(比如登录检查,没登录就返回false,中断请求),然后执行Controller方法本身,接着执行拦截器的postHandle(比如给视图加公共数据)。我之前做登录拦截器时,就遇到过preHandle返回false的情况——用户没登录,拦截器返回false,结果后续的Controller方法没执行,但已经通过的拦截器的afterCompletion会倒序执行,用来清理资源(比如关闭数据库连接)。第四个工作是“渲染视图”:要是方法返回ModelAndView,doDispatch会调用ViewResolver找到对应的视图文件(比如Thymeleaf的.html模板),把Model里的数据填充进去。我之前做项目,视图总是渲染失败,后来查ViewResolver的源码,发现我把prefix配置成了“/templates/”,但模板文件放在“/views/”下面,ViewResolver拼接出来的路径是“/templates/index.html”,自然找不到文件,改了prefix就好了。
其实doDispatch的逻辑没那么复杂,就是“找方法→找帮手→执行流程→渲染视图”四步,我当初为了搞懂它,特意把源码打印出来逐行标注释,现在不管遇到什么请求分发的问题,对照这四步就能快速定位。比如你遇到请求参数绑错的问题,就去看Adapter的Converter;遇到视图找不到的问题,就去看ViewResolver的路径配置;遇到拦截器不生效的问题,就去看HandlerExecutionChain里的拦截器顺序——这些问题的根源,其实都在doDispatch的流程里。
拦截器突然不生效,可能是什么原因?
首先检查拦截器的路径配置(比如是否误将“/api/”写成“/apis/”,导致请求未匹配);其次看拦截器在HandlerExecutionChain中的顺序——若前面的拦截器preHandle返回false,后续拦截器不会执行;还要确认preHandle方法是否返回true(返回false会直接中断请求)。可以通过在HandlerExecutionChain的applyPreHandle方法打断点,跟踪拦截器的实际执行顺序。
DispatcherServlet的核心方法是什么?主要负责哪些工作?
核心方法是doDispatch,它是Spring MVC处理请求的“总流程控制器”,主要做四件事:①通过HandlerMapping找到对应请求的Handler(比如@Controller里的方法);②通过HandlerAdapter找到执行该Handler的“适配器”(比如处理参数绑定的RequestMappingHandlerAdapter);③执行拦截器的preHandle、Handler本身的业务逻辑、拦截器的postHandle;④通过ViewResolver渲染ModelAndView(将数据填充到视图模板,比如Thymeleaf或JSP)。
拦截器的三个方法执行顺序有什么规律?preHandle返回false会影响后续方法吗?
preHandle按拦截器配置的正序执行(比如拦截器A→B→C,preHandle顺序是A→B→C);postHandle按逆序执行(C→B→A),但仅当所有拦截器的preHandle都返回true时才会触发;afterCompletion也按逆序执行(C→B→A),且无论preHandle是否返回true,只要Handler(Controller方法)执行过就会执行(用于资源清理,比如关闭流)。若某个拦截器preHandle返回false,后续拦截器的preHandle、postHandle都不会执行,但已通过的拦截器的afterCompletion会倒序执行。
参数绑定异常(比如字符串转LocalDate失败),怎么从源码定位问题?
参数绑定的核心逻辑在HandlerAdapter(比如RequestMappingHandlerAdapter)中,它会调用Converter(转换器)将请求参数转换为目标类型(比如把“2024-05-20”转成LocalDate)。若转换失败,通常是因为缺少对应的Converter。可以查看RequestMappingHandlerAdapter的getDefaultArgumentResolvers方法,确认是否有处理LocalDate的参数解析器;若没有,可自定义Converter(比如CustomLocalDateConverter)并注册到HandlerAdapter中,就能解决转换问题。