在享受了@RequestMapping方便的处理映射时,忍不住会开始好奇,
- 这个注解怎么就能找到我的接口?
- @RequestMapping到底替我做了什么事?
今天来解答如上两个问题。
画了个比较难看的图,不过都是老生常谈了,逻辑都是熟悉的。
SpringMVC在处理一个请求时,会首先通过请求头中的Request URL中的信息,查找Spring的映射处理器,顾名思义,既然是映射,那么一定存在一个键值对,存在一一对应的关系。
那我来找一下这个映射保存在哪里了。
由于是学习DispatcherServlet,就直接写个简单的SpringBoot程序,打上断点,在idea的debug模式中,查看调用链,或者叫有的人称呼为栈帧。
在这里明显发现,在进入接口前(从上往下观察调用链),经历了好几次的反射,然后是RequestMappingHandlerAdapter(这个类比较重要等一下聊),再然后是DispatcherServlet。
马上点进DispatcherServlet,发现一行亲切的注释。
原来这里就是实际调用处理程序的地方。
检查一下变量,发现这个处理程序就是我的接口,证明映射已经找好了。
我今天要查的是@RequestMapping是怎么找到我接口的,所以往上翻,查查这个mappedHandler到底是怎么来的。
翻了几行就看到了,而且也有一行亲切的注释。
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
确定当前请求的处理程序。
很明显,就它了。继续点进去看。
这是一条比较长的调用链,中间过程就跳过了,直接看结果。
urlPath就是从Request Header中提取的URL。
pathLookup则是我这个程序中的全部接口的路径。
可以发现这个就是最开始的图中,处理映射的关键部分了。
SpringMVC通过维护pathLookup这个Map来处理URL与接口的映射关系。
但是又有一个问题,这不是一对一呀。MultiValueMap明显是一个key对应几个value的结构。
写过bug的都知道,一个程序中,@RequestMapping写重复了是起不来项目的。
为什么SpringMVC要选择这种结构呢?等下面再分析。
解决了@RequestMapping是怎么找到我的接口的问题了
现在想知道,@RequestMapping为了能找到我的接口,背地里都做了些什么
为了了解pathLookup的初始化过程,就必须要了解SpringMVC的加载过程。
还是看AbstractHandlerMethodMapping,这里发现是一个循环中添加的。
而且在register方法632行,居然还加了读写锁的写锁,什么情况,难道这里还有并发问题?
先留下问题继续Alt+F7查上游调用链。
(这里补充一点,Alt+F7不好使多半是GeForce Experience的游戏内覆盖模块占用了快捷键,建议改一下或者干脆关掉)
在register内打上断点,重启项目,发现进断点了,看一下调用链。
又引出一个类,RequestMappingHandlerMapping。
跟刚才关注的RequestMappingHandlerAdapter长得可以说是如同亲兄弟。
由于个人水平有限和篇幅原因,这两兄弟只能在下篇文章中聊一下个人的简单认识。
从上图中的调用链可以看出,最终调用是在afterPropertiesSet。
这是一个InitializingBean接口中的方法,实现了InitializingBean的类必须重写afterPropertiesSet,而afterPropertiesSet将会在类实例化以后立刻调用。
而在AbstractHandlerMethodMapping的afterPropertiesSet中调用的是initHandlerMethods。
注释说的很明白,这里做两件事,第一件事是扫描所有的bean,第二件事是注册处理程序,也就是刚刚聊的建立映射关系。
通过ApplicationContext获取到所有Object类的子类,可以看到包含了程序中的controller。
再判断是否是被@Controller或者是@RequestMapping修饰的bean,由于暴露的接口通常都必须被@Controller或者是@RequestMapping修饰,所以可以理解为筛选需要建立映射的bean。
回到RequestMappingHandlerMapping的实现方法。
继续步进看实现。
需要注意的是这里的MethodIntrospector.MetadataLookup是一个@FunctionalInterface,所以这里的匿名实现不包含函数名。
MethodIntrospector.selectMethods返回的map,就是这个类中所有被@RequestMapping修饰的方法。key为Method对象,value是RequestMappingInfo。
RequestMappingInfo可以理解为RequestMapping的实体类,对我们有用的信息是其中包含的pathPattern,也就是@RequestMapping的value。
在获取了bean中包含的@RequestMapping修饰的方法后,又回到了之前的register,将使用RequestMappingInfo中的信息,为pathLookup赋值,这样一来,当请求进入程序时,就能够找到正确的接口了。
再回顾一下SpringMVC中bean的生命周期。
再拿一张小图做参考。
总结一下,@RequestMapping的初始化过程应当理解为:
@RequestMapping通过pathLookup维护了一个MultiValueMap类型的变量pathLookup,pathLookup是通过RequestMappingHandlerMapping初始化的,而这个初始化的过程发生在Spring容器初始化bean的过程中。