我把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解码)。