我把Spring请求URL找到相应Controller的过程调试了一遍,在此记录该过程,属于个人记录,过程中存在不细致甚至错误的地方,还请见谅。
代码版本为spring-webmvc-5.2.5.RELEASE-sources.jar
分析
分析入口:org.springframework.web.servlet.DispatcherServlet#doDispatch
跟进getHandler
再跟进mapping.getHandler
,它是一个interface
,由AbstractHandlerMapping
实现此接口
点击148行右侧的绿色图标,跳转到AbstractHandlerMapping#getHandler
此处getHandlerIngernal
是一个抽象方法,在AbstractHandlerMethodMapping#getHandlerInternal
中实现了该抽象方法
getLookupPathForRequest
重点在此,getUrlPathHelper().getLookupPathForRequest(request)
中从请求中获取代码中对应的path,可以在此下断点进行调试,详细了解Spring是怎么获取请求路径的。
传入http://localhost/shiro/admin;////..;/manage/page/index
进行调试(如果此时传入正常的URL,则getPathWithinServletMapping
方法会返回空字符串,再到else
中的getPathWithinAPPlication
方法)
跟进getPathWithinServletMapping
getPathWithinApplication
首先来看getPathWithinApplication
其中getContextPath
为获取Context path,不做重点分析,重点来看getRequestUri
request.getRequestURI()
得到的是/shiro/admin;////..;/manage/page/index
,继续跟进decodeAndCleanUriString
方法
decodeAndCleanUriString
方法对uri
进行了三种处理
(1) 移除两个/
间,分号和分号后的内容,得到/shiro/admin////../manage/page/index
(例如传入/admin;random/page;jsessionid=1111
,会得到/admin/page/
)
(2) URL解码
(3) 移除多余的/
经过以上处理,最终得到的uri是/shiro/admin/../manage/page/index
回到getPathWithinApplication
方法,其中getRemainingPath
方法将移除requestUri
中和contextPath
中相同的字段(在此是/shiro
),得到/admin/../manage/page/index
getServletPath
也就是通过request.getServletPath()
获取Servlet路径,得到/manage/page/index
getPathWithinApplication 2
经过以上代码,回到getPathWithinApplication
。其中第218行sanitizedPathWithinApp
变量,再次移除多余的/
,uri在此无变化。
之后存在一个if判断,当sanitizedPathWithinApp
不是servletPath
的子集时,path
等于getRemainingPath(pathWithinApp, servletPath, false)
此时getRequestURI
得到的/admin/../manage/page/index
和getServletPath
获取到的/manage/page/index
开头无法匹配,经过getRemainingPath
得到的path为null
。
经过以上代码处理,最终返回的是servletPath
,也就是返回/manage/page/index
。
这里多说一下,通过getReqeustURI
和getServletPath
进行对比,当两者不同时,尝试getPathInfo
。若PathInfo
为空,最后返回SerletPath
。通过这几个if判断,确保能正确获取请求路径(因为getReqeustURI
得到的结果不可信,利用各种URL兼容性难以通过URI判断实际请求路径)。
lookupHandlerMethod
得到请求的资源路径之后,回到AbstractHandlerMethodMapping#getHandlerInternal
,调用lookupHandlerMethod
方法
看该方法的描述,将资源路径进行最合适的匹配,得到最合适的Controller方法。
Look up the best-matching handler method for the current request.
If multiple matches are found, the best match is selected.
它定义了一个matchs
的列表,然后内存变量mappingRegistry
根据lookupPath
获取所有的匹配对象。
如果存在这个匹配对象,那么回调用addMatchingMappings
方法将匹配的对象映射到matches
上。
如果未匹配到这个lookupPath
,那么则会循环mappingRegistry
中每个Mapping来匹配该路劲(因为RequestMapping中定义的路劲可以有通配符)。
至于if(!matches.isEmpty())
内的代码,只是看看有没有一个请求映射到多个处理方法,如果存在并且优先级还一样,那么就会报错:Ambiguous handler methods mapped for HTTP path。
来看addMatchingMappings
的代码
注意到AbstractHandlerMethodMapping
这个class是个泛型抽象类 getMatchingMapping
是个抽象方法,返回值为定义的泛型(T),我们暂且把这个泛型理解成[映射信息]。
根据方法名称,T getMatchingMapping(T,reqeust)
,根据映射信息和请求获取是否匹配,如果匹配则返回映射信息。
RequestMappingInfoHandlerMapping
这个class继承了这个抽象类,泛型类型为:RequestMappingInfo
。看看它的方法实现
如果其中methods
、params
、headers
、consumes
、produces
、patterns
、custom
都匹配,那么可以说这个[映射信息]就匹配成功了。如果未匹配成功,则返回404。
methods : 请求方法匹配(POST/GET/DELETE/PUT等等),对应RequestMapping注解的method
params : 请求参数匹配 例如myParam=myValue,对应RequestMapping注解的params
headers : 请求头信息(例如:User-Agent: Mozilla/5.0),对应RequestMapping的headers
consumes : 提交内容类型(例如:application/json, text/html;),对应RequestMapping的consumes
produces : 返回的内容类型(例如:application/json),对应RequestMapping的consumes
patterns : URL格式(例如:test/testDo),对应RequestMapping的value
自定义条件
根据以上匹配得到的信息,就能找到对应的Controller。
org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
总结
Spring中获取请求路径的过程,在org.springframework.web.util.UrlPathHelper#getPathWithinServletMapping
中,其流程大概如下图所示。
其中比较关键的点是getPathWithinApplication
方法,其中对RequestURI的处理有:删除冒号及冒号后的内容,URL解码,去除重复斜杠/
(注意是先删除冒号,再URL解码)。