SpringMVC原始碼分析4:DispatcherServlet如何找到正確的Controller
SpringMVC是目前主流的Web MVC框架之一。
我們使用瀏覽器通過地址 http://ip:port/contextPath/path進行訪問,SpringMVC是如何得知使用者到底是訪問哪個Controller中的方法,這期間到底發生了什麼。
本文將分析SpringMVC是如何處理請求與Controller之間的對映關係的,讓讀者知道這個過程中到底發生了什麼事情。
本文實際上是在上文基礎上,深入分析
HandlerMapping裡的
HandlerExecutionChain getHandler(HttpServletRequest var1) throws Exception;
該方法的具體實現,包括它如何找到對應的方法,以及如何把結果儲存在map裡,以便讓請求轉發到對應的handler上,同時也分析了handleradaptor具體做了什麼事情。
原始碼分析
在分析原始碼之前,我們先了解一下幾個東西。
1.這個過程中重要的介面和類。
HandlerMethod類:
Spring3.1版本之後引入的。 是一個封裝了方法引數、方法註解,方法返回值等眾多元素的類。
它的子類InvocableHandlerMethod有兩個重要的屬性WebDataBinderFactory和HandlerMethodArgumentResolverComposite, 很明顯是對請求進行處理的。
InvocableHandlerMethod的子類ServletInvocableHandlerMethod有個重要的屬性HandlerMethodReturnValueHandlerComposite,很明顯是對響應進行處理的。
ServletInvocableHandlerMethod這個類在HandlerAdapter對每個請求處理過程中,都會例項化一個出來(上面提到的屬性由HandlerAdapter進行設定),分別對請求和返回進行處理。 (RequestMappingHandlerAdapter原始碼,例項化ServletInvocableHandlerMethod的時候分別set了上面提到的重要屬性)
MethodParameter類:
HandlerMethod類中的parameters屬性型別,是一個MethodParameter陣列。MethodParameter是一個封裝了方法引數具體資訊的工具類,包括引數的的索引位置,型別,註解,引數名等資訊。
HandlerMethod在例項化的時候,建構函式中會初始化這個陣列,這時只初始化了部分資料,在HandlerAdapter對請求處理過程中會完善其他屬性,之後交予合適的HandlerMethodArgumentResolver介面處理。
以類DeptController為例:
@Controller
@RequestMapping(value = "/dept")
public class DeptController {
@Autowired
private IDeptService deptService;
@RequestMapping("/update")
@ResponseBody
public String update(Dept dept) {
deptService.saveOrUpdate(dept);
return "success";
}
}
(剛初始化時的資料)
(HandlerAdapter處理後的資料)
RequestCondition介面:
Spring3.1版本之後引入的。 是SpringMVC的對映基礎中的請求條件,可以進行combine, compareTo,getMatchingCondition操作。這個介面是對映匹配的關鍵介面,其中getMatchingCondition方法關乎是否能找到合適的對映。
RequestMappingInfo類:
Spring3.1版本之後引入的。 是一個封裝了各種請求對映條件並實現了RequestCondition介面的類。
有各種RequestCondition實現類屬性,patternsCondition,methodsCondition,paramsCondition,headersCondition,consumesCondition以及producesCondition,這個請求條件看屬性名也瞭解,分別代表http請求的路徑模式、方法、引數、頭部等資訊。
RequestMappingHandlerMapping類:
處理請求與HandlerMethod對映關係的一個類。
2.Web伺服器啟動的時候,SpringMVC到底做了什麼。
先看AbstractHandlerMethodMapping的initHandlerMethods方法中。
我們進入createRequestMappingInfo方法看下是如何構造RequestMappingInfo物件的。
PatternsRequestCondition建構函式:
類對應的RequestMappingInfo存在的話,跟方法對應的RequestMappingInfo進行combine操作。
然後使用符合條件的method來註冊各種HandlerMethod。
下面我們來看下各種RequestCondition介面的實現類的combine操作。
PatternsRequestCondition:
RequestMethodsRequestCondition:
方法的請求條件,用個set直接add即可。
其他相關的RequestConditon實現類讀者可自行檢視原始碼。
最終,RequestMappingHandlerMapping中兩個比較重要的屬性
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();
private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();
T為RequestMappingInfo。
構造完成。
我們知道,SpringMVC的分發器DispatcherServlet會根據瀏覽器的請求地址獲得HandlerExecutionChain。
這個過程我們看是如何實現的。
首先看HandlerMethod的獲得(直接看關鍵程式碼了):
這裡的比較器是使用RequestMappingInfo的compareTo方法(RequestCondition介面定義的)。
然後構造HandlerExecutionChain加上攔截器
例項
寫了這麼多,來點例子讓我們驗證一下吧。
@Controller
@RequestMapping(value = "/wildcard")
public class TestWildcardController {
@RequestMapping("/test/**")
@ResponseBody
public String test1(ModelAndView view) {
view.setViewName("/test/test");
view.addObject("attr", "TestWildcardController -> /test/**");
return view;
}
@RequestMapping("/test/*")
@ResponseBody
public String test2(ModelAndView view) {
view.setViewName("/test/test");
view.addObject("attr", "TestWildcardController -> /test*");
return view;
}
@RequestMapping("test?")
@ResponseBody
public String test3(ModelAndView view) {
view.setViewName("/test/test");
view.addObject("attr", "TestWildcardController -> test?");
return view;
}
@RequestMapping("test/*")
@ResponseBody
public String test4(ModelAndView view) {
view.setViewName("/test/test");
view.addObject("attr", "TestWildcardController -> test/*");
return view;
}
}
由於這裡的每個pattern都帶了*因此,都不會加入到urlMap中,但是handlerMethods還是有的。
當我們訪問:http://localhost:8888/SpringMVCDemo/wildcard/test1的時候。
會先根據 "/wildcard/test1" 找urlMap對應的RequestMappingInfo集合,找不到的話取handlerMethods集合中所有的key集合(也就是RequestMappingInfo集合)。
然後進行匹配,匹配根據RequestCondition的getMatchingCondition方法。
最終匹配到2個RequestMappingInfo:
然後會使用比較器進行排序。
之前也分析過,比較器是有優先順序的。
我們看到,RequestMappingInfo除了pattern,其他屬性都是一樣的。
我們看下PatternsRequestCondition比較的邏輯:
因此,/test*的萬用字元比/test?的多,因此,最終選擇了/test?
直接比較優先於萬用字元。
@Controller
@RequestMapping(value = "/priority")
public class TestPriorityController {
@RequestMapping(method = RequestMethod.GET)
@ResponseBody
public String test1(ModelAndView view) {
view.setViewName("/test/test");
view.addObject("attr", "其他condition相同,帶有method屬性的優先順序高");
return view;
}
@RequestMapping()
@ResponseBody
public String test2(ModelAndView view) {
view.setViewName("/test/test");
view.addObject("attr", "其他condition相同,不帶method屬性的優先順序高");
return view;
}
}
這裡例子,其他requestCondition都一樣,只有RequestMethodCondition不一樣。
看出,方法多的優先順序越多。
至於其他的RequestCondition,大家自行檢視原始碼吧。
資原始檔對映
以上分析均是基於Controller方法的對映(RequestMappingHandlerMapping)。
SpringMVC中還有靜態檔案的對映,SimpleUrlHandlerMapping。
DispatcherServlet找對應的HandlerExecutionChain的時候會遍歷屬性handlerMappings,這個一個實現了HandlerMapping介面的集合。
由於我們在*-dispatcher.xml中加入了以下配置:
<mvc:resources location="/static/" mapping="/static/**"/>
Spring解析配置檔案會使用ResourcesBeanDefinitionParser進行解析的時候,會例項化出SimpleUrlHandlerMapping。
其中註冊的HandlerMethod為ResourceHttpRequestHandler。
訪問地址:http://localhost:8888/SpringMVCDemo/static/js/jquery-1.11.0.js
地址匹配到/static/**。
最終SimpleUrlHandlerMapping找到對應的Handler -> ResourceHttpRequestHandler。
ResourceHttpRequestHandler進行handleRequest的時候,直接輸出資原始檔的文字內容。
總結
大致上整理了一下SpringMVC對請求的處理,包括其中比較關鍵的類和介面,希望對讀者有幫助。
讓自己對SpringMVC有了更深入的認識,也為之後分析資料繫結,攔截器、HandlerAdapter等打下基礎