0 摘要
本文從源碼層面簡單講解SpringMVC的處理器映射環節,也就是查找Controller詳細過程
1 SpringMVC請求流程
Controller查找在上圖中對應的步驟1至2的過程
SpringMVC詳細運行流程圖
2 SpringMVC初始化過程
2.1 先認識兩個類
1.RequestMappingInfo
封裝RequestMapping註解
包含HTTP請求頭的相關信息
一個實例對應一個RequestMapping註解
2.HandlerMethod
封裝Controller的處理請求方法
包含該方法所屬的bean對象、該方法對應的method對象、該方法的參數等
RequestMappingHandlerMapping的繼承關係
在SpringMVC初始化的時候
首先執行RequestMappingHandlerMapping的afterPropertiesSet
然後進入AbstractHandlerMethodMapping的afterPropertiesSet
這個方法會進入該類的initHandlerMethods
負責從applicationContext中掃描beans,然後從bean中查找並註冊處理器方法
//Scan beans in the ApplicationContext, detect and register handler methods.protected void initHandlerMethods() { ... //獲取applicationContext中所有的bean name String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) : getApplicationContext().getBeanNamesForType(Object.class)); //遍歷beanName數組for (String beanName : beanNames) { //isHandler會根據bean來判斷bean定義中是否帶有Controller註解或RequestMapping註解if (isHandler(getApplicationContext().getType(beanName))){ detectHandlerMethods(beanName); } } handlerMethodsInitialized(getHandlerMethods());} RequestMappingHandlerMapping#isHandler
上圖方法即判斷當前bean定義是否帶有Controlller註解或RequestMapping註解
如果只有RequestMapping生效嗎?不會的!
因為這種情況下Spring初始化的時候不會把該類註冊為Spring bean,遍歷beanNames時不會遍歷到這個類,所以這裡把Controller換成Compoent也可以,不過一般不這麼做
當確定bean為handler後,便會從該bean中查找出具體的handler方法(即Controller類下的具體定義的請求處理方法),查找代碼如下
/** * Look for handler methods in a handler * @param handler the bean name of a handler or a handler instance */protected void detectHandlerMethods(final Object handler) { //獲取當前Controller bean的class對象Class<?> handlerType = (handler instanceof String) ? getApplicationContext().getType((String) handler) : handler.getClass(); //避免重複調用getMappingForMethod 來重建RequestMappingInfo 實例final Map<Method, T> mappings = new IdentityHashMap<Method, T>(); //同上,也是該Controller bean的class對象final Class<?> userType = ClassUtils.getUserClass(handlerType); //獲取當前bean的所有handler method //根據method 定義是否帶有RequestMapping //若有則創建RequestMappingInfo實例Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() { @Override public boolean matches(Method method) { T mapping = getMappingForMethod(method, userType); if (mapping != null) { mappings.put(method, mapping); return true; } else { return false; } } }); //遍歷並註冊當前bean的所有handler method for (Method method : methods) { //註冊handler method,進入以下方法registerHandlerMethod(handler, method, mappings.get(method)); }以上代碼有兩個地方有調用了getMappingForMethod
使用方法和類型級別RequestMapping註解來創建RequestMappingInfo
@Override protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { RequestMappingInfo info = null; //獲取method的@RequestMapping RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); if (methodAnnotation != null) { RequestCondition<?> methodCondition = getCustomMethodCondition(method); info = createRequestMappingInfo(methodAnnotation, methodCondition); //獲取method所屬bean的@RequtestMapping註解RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); if (typeAnnotation != null) { RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType); //合併兩個@RequestMapping註解info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info); } } return info; }這個方法的作用就是根據handler method方法創建RequestMappingInfo對象。首先判斷該mehtod是否含有RequestMpping註解。如果有則直接根據該註解的內容創建RequestMappingInfo對象。創建以後判斷當前method所屬的bean是否也含有RequestMapping註解。如果含有該註解則會根據該類上的註解創建一個RequestMappingInfo對象。然後在合併method上的RequestMappingInfo對象,最後返回合併後的對象。現在回過去看detectHandlerMethods方法,有兩處調用了getMappingForMethod方法,個人覺得這裡是可以優化的,在第一處判斷method時否為handler時,創建的RequestMappingInfo對象可以保存起來,直接拿來後面使用,就少了一次創建RequestMappingInfo對象的過程。然後緊接著進入registerHandlerMehtod方法,如下
protected void registerHandlerMethod(Object handler, Method method, T mapping) { //創建HandlerMethod HandlerMethod newHandlerMethod = createHandlerMethod(handler, method); HandlerMethod oldHandlerMethod = handlerMethods.get(mapping); //檢查配置是否存在歧義性if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) { throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() + "' bean method /n" + newHandlerMethod + "/nto " + mapping + ": There is already '" + oldHandlerMethod.getBean() + "' bean method/n" + oldHandlerMethod + " mapped."); } this.handlerMethods.put(mapping, newHandlerMethod); if (logger.isInfoEnabled()) { logger.info("Mapped /"" + mapping + "/" onto " + newHandlerMethod); } //獲取@RequestMapping註解的value,然後添加value->RequestMappingInfo映射記錄至urlMap中Set<String> patterns = getMappingPathPatterns(mapping); for (String pattern : patterns) { if (!getPathMatcher().isPattern(pattern)) { this.urlMap.add(pattern, mapping); } }}這裡T的類型是RequestMappingInfo。這個對象就是封裝的具體Controller下的方法的RequestMapping註解的相關信息。一個RequestMapping註解對應一個RequestMappingInfo對象。 HandlerMethod和RequestMappingInfo類似,是對Controlelr下具體處理方法的封裝。先看方法的第一行,根據handler和mehthod創建HandlerMethod對象。第二行通過handlerMethods map來獲取當前mapping對應的HandlerMethod。然後判斷是否存在相同的RequestMapping配置。如下這種配置就會導致此處拋Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping found. Cannot map...
異常
@Controller@RequestMapping("/AmbiguousTest")public class AmbiguousTestController { @RequestMapping(value = "/test1") @ResponseBody public String test1(){ return "method test1"; } @RequestMapping(value = "/test1") @ResponseBody public String test2(){ return "method test2"; }}在SpingMVC啟動(初始化)階段檢查RequestMapping配置是否有歧義,這是其中一處檢查歧義的(後面還會提到一個在運行時檢查歧義性的地方)。然後確認配置正常以後會把該RequestMappingInfo和HandlerMethod對象添加至handlerMethods(LinkedHashMap)中,靜接著把RequestMapping註解的value和ReuqestMappingInfo對象添加至urlMap中。
registerHandlerMethod方法簡單總結
該方法的主要有3個職責
1. 檢查RequestMapping註解配置是否有歧義。
2. 構建RequestMappingInfo到HandlerMethod的映射map。該map便是AbstractHandlerMethodMapping的成員變量handlerMethods。 LinkedHashMap。
3. 構建AbstractHandlerMethodMapping的成員變量urlMap,MultiValueMap。這個數據結構可以把它理解成Map>。其中String類型的key存放的是處理方法上RequestMapping註解的value。就是具體的uri
先有如下Controller
@Controller@RequestMapping("/UrlMap")public class UrlMapController { @RequestMapping(value = "/test1", method = RequestMethod.GET) @ResponseBody public String test1(){ return "method test1"; } @RequestMapping(value = "/test1") @ResponseBody public String test2(){ return "method test2"; } @RequestMapping(value = "/test3") @ResponseBody public String test3(){ return "method test3"; }}初始化完成後,對應AbstractHandlerMethodMapping的urlMap的結構如下
以上便是SpringMVC初始化的主要過程
查找過程
為了理解查找流程,帶著一個問題來看,現有如下Controller
@Controller@RequestMapping("/LookupTest")public class LookupTestController { @RequestMapping(value = "/test1", method = RequestMethod.GET) @ResponseBody public String test1(){ return "method test1"; } @RequestMapping(value = "/test1", headers = "Referer=https://www.baidu.com") @ResponseBody public String test2(){ return "method test2"; } @RequestMapping(value = "/test1", params = "id=1") @ResponseBody public String test3(){ return "method test3"; } @RequestMapping(value = "/*") @ResponseBody public String test4(){ return "method test4"; }}有如下請求
這個請求會進入哪一個方法?
web容器(Tomcat、jetty)接收請求後,交給DispatcherServlet處理。 FrameworkServlet調用對應請求方法(eg:get調用doGet),然後調用processRequest方法。進入processRequest方法後,一系列處理後,在line:936進入doService方法。然後在Line856進入doDispatch方法。在line:896獲取當前請求的處理器handler。然後進入AbstractHandlerMethodMapping的lookupHandlerMethod方法。代碼如下
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<Match>(); //根據uri獲取直接匹配的RequestMappingInfos List<T> directPathMatches = this.urlMap.get(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } //不存在直接匹配的RequetMappingInfo,遍歷所有RequestMappingInfo if (matches.isEmpty()) { // No choice but to go through all mappings addMatchingMappings(this.handlerMethods.keySet(), matches, request); } //獲取最佳匹配的RequestMappingInfo對應的HandlerMethod if (!matches.isEmpty()) { Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); Collections.sort(matches, comparator); if (logger.isTraceEnabled()) { logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches); } //再一次檢查配置的歧義性Match bestMatch = matches.get(0); if (matches.size() > 1) { Match secondBestMatch = matches.get(1); if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.handlerMethod.getMethod(); Method m2 = secondBestMatch.handlerMethod.getMethod(); throw new IllegalStateException( "Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" + m1 + ", " + m2 + "}"); } } handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.handlerMethod; } else { return handleNoMatch(handlerMethods.keySet(), lookupPath, request); }}進入lookupHandlerMethod方法,其中lookupPath="/LookupTest/test1",根據lookupPath,也就是請求的uri。直接查找urlMap,獲取直接匹配的RequestMappingInfo list。這裡會匹配到3個RequestMappingInfo。如下
然後進入addMatchingMappings方法
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) { for (T mapping : mappings) { T match = getMatchingMapping(mapping, request); if (match != null) { matches.add(new Match(match, handlerMethods.get(mapping))); } }}這個方法的職責是遍歷當前請求的uri和mappings中的RequestMappingInfo能否匹配上,如果能匹配上,創建一個相同的RequestMappingInfo對象。再獲取RequestMappingInfo對應的handlerMethod。然後創建一個Match對象添加至matches list中。執行完addMatchingMappings方法,回到lookupHandlerMethod。這時候matches還有3個能匹配上的RequestMappingInfo對象。接下來的處理便是對matchers列表進行排序,然後獲取列表的第一個元素作為最佳匹配。返回Match的HandlerMethod。這裡進入RequestMappingInfo的compareTo方法,看一下具體的排序邏輯。代碼如下
public int compareTo(RequestMappingInfo other, HttpServletRequest request) { int result = patternsCondition.compareTo(other.getPatternsCondition(), request); if (result != 0) { return result; } result = paramsCondition.compareTo(other.getParamsCondition(), request); if (result != 0) { return result; } result = headersCondition.compareTo(other.getHeadersCondition(), request); if (result != 0) { return result; } result = consumesCondition.compareTo(other.getConsumesCondition(), request); if (result != 0) { return result; } result = producesCondition.compareTo(other.getProducesCondition(), request); if (result != 0) { return result; } result = methodsCondition.compareTo(other.getMethodsCondition(), request); if (result != 0) { return result; } result = customConditionHolder.compareTo(other.customConditionHolder, request); if (result != 0) { return result; } return 0;}代碼裡可以看出,匹配的先後順序是value>params>headers>consumes>produces>methods>custom,看到這裡,前面的問題就能輕易得出答案了。在value相同的情況,params更能先匹配。所以那個請求會進入test3()方法。再回到lookupHandlerMethod,在找到HandlerMethod。 SpringMVC還會這裡再一次檢查配置的歧義性,這裡檢查的原理是通過比較匹配度最高的兩個RequestMappingInfo進行比較。此處可能會有疑問在初始化SpringMVC有檢查配置的歧義性,這里為什麼還會檢查一次。假如現在Controller中有如下兩個方法,以下配置是能通過初始化歧義性檢查的。
@RequestMapping(value = "/test5", method = {RequestMethod.GET, RequestMethod.POST})@ResponseBodypublic String test5(){ return "method test5";}@RequestMapping(value = "/test5", method = {RequestMethod.GET, RequestMethod.DELETE})@ResponseBodypublic String test6(){ return "method test6";}現在執行http://localhost:8080/SpringMVC-Demo/LookupTest/test5 請求,便會在lookupHandlerMethod方法中拋java.lang.IllegalStateException: Ambiguous handler methods mapped for HTTP path 'http://localhost:8080/SpringMVC-Demo/LookupTest/test5'異常。這裡拋該異常是因為RequestMethodsRequestCondition的compareTo方法是比較的method數。代碼如下
public int compareTo(RequestMethodsRequestCondition other, HttpServletRequest request) { return other.methods.size() - this.methods.size();}什麼時候匹配通配符?當通過urlMap獲取不到直接匹配value的RequestMappingInfo時才會走通配符匹配進入addMatchingMappings方法。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。