SpringMVC源碼之Controller查找原理

摘要

  • 本文從源碼層面簡(jiǎn)單講解SpringMVC的處理器映射環(huán)節(jié),也就是查找Controller詳細(xì)過(guò)程。

SpringMVC請(qǐng)求流程

image.png
  • Controller查找在上圖中對(duì)應(yīng)的步驟1至2的過(guò)程

SpringMVC初始化過(guò)程

理解初始化過(guò)程之前,先認(rèn)識(shí)兩個(gè)類

  1. RequestMappingInfo類,對(duì)RequestMapping注解封裝。里面包含http請(qǐng)求頭的相關(guān)信息。如uri、method、params、header等參數(shù)。一個(gè)對(duì)象對(duì)應(yīng)一個(gè)RequestMapping注解
  2. HandlerMethod類,是對(duì)Controller的處理請(qǐng)求方法的封裝。里面包含了該方法所屬的bean對(duì)象、該方法對(duì)應(yīng)的method對(duì)象、該方法的參數(shù)等。


    image.png
  • 上圖是RequestMappingHandlerMapping的繼承關(guān)系。在SpringMVC初始化的時(shí)候,首先執(zhí)行RequestMappingHandlerMapping中的afterPropertiesSet方法,然后會(huì)進(jìn)入AbstractHandlerMethodMapping的afterPropertiesSet方法(line:93),這個(gè)方法會(huì)進(jìn)入當(dāng)前類的initHandlerMethods方法(line:103)。這個(gè)方法的職責(zé)便是從applicationContext中掃描beans,然后從bean中查找并注冊(cè)處理器方法,代碼如下。
protected void initHandlerMethods() {
  if (logger.isDebugEnabled()) {
      logger.debug("Looking for request mappings in application context: " + getApplicationContext());
  }
  //獲取applicationContext中所有的bean name
  String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
        BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
        getApplicationContext().getBeanNamesForType(Object.class));
  //遍歷beanName數(shù)組
  for (String beanName : beanNames) {
      //isHandler會(huì)根據(jù)bean來(lái)判斷bean定義中是否帶有Controller注解或RequestMapping注解
      if (isHandler(getApplicationContext().getType(beanName))){
        detectHandlerMethods(beanName);
      }
  }
  handlerMethodsInitialized(getHandlerMethods());
}
  • isHandler方法其實(shí)很簡(jiǎn)單,如下
@Override
protected boolean isHandler(Class<?> beanType) {
  return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
        (AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null));
}
  • 就是判斷當(dāng)前bean定義是否帶有Controlller注解或RequestMapping注解,看了這里邏輯可能會(huì)想如果只有RequestMapping會(huì)生效嗎?答案是不會(huì)的,因?yàn)樵谶@種情況下Spring初始化的時(shí)候不會(huì)把該類注冊(cè)為Spring bean,遍歷beanNames時(shí)不會(huì)遍歷到這個(gè)類,所以這里把Controller換成Compoent注解也是可以,不過(guò)一般不會(huì)這么做。當(dāng)確定bean為handlers后,便會(huì)從該bean中查找出具體的handler方法(也就是我們通常定義的Controller類下的具體定義的請(qǐng)求處理方法),查找代碼如下
protected void detectHandlerMethods(final Object handler) {
  //獲取到當(dāng)前Controller bean的class對(duì)象
  Class<?> handlerType = (handler instanceof String) ?
        getApplicationContext().getType((String) handler) : handler.getClass();
  //同上,也是該Controller bean的class對(duì)象
  final Class<?> userType = ClassUtils.getUserClass(handlerType);
  //獲取當(dāng)前bean的所有handler method。這里查找的依據(jù)便是根據(jù)method定義是否帶有RequestMapping注解。如果有根據(jù)注解創(chuàng)建RequestMappingInfo對(duì)象
  Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
      public boolean matches(Method method) {
        return getMappingForMethod(method, userType) != null;
      }
  });
  //遍歷并注冊(cè)當(dāng)前bean的所有handler method
  for (Method method : methods) {
      T mapping = getMappingForMethod(method, userType);
      //注冊(cè)handler method,進(jìn)入以下方法
      registerHandlerMethod(handler, method, mapping);
  }
}
  • 以上代碼有兩個(gè)地方有調(diào)用了getMappingForMethod方法
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);
        //合并兩個(gè)@RequestMapping注解
        info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
      }
  }
  return info;
}
  • 這個(gè)方法的作用就是根據(jù)handler method方法創(chuàng)建RequestMappingInfo對(duì)象。首先判斷該mehtod是否含有RequestMpping注解。如果有則直接根據(jù)該注解的內(nèi)容創(chuàng)建RequestMappingInfo對(duì)象。創(chuàng)建以后判斷當(dāng)前method所屬的bean是否也含有RequestMapping注解。如果含有該注解則會(huì)根據(jù)該類上的注解創(chuàng)建一個(gè)RequestMappingInfo對(duì)象。然后在合并method上的RequestMappingInfo對(duì)象,最后返回合并后的對(duì)象?,F(xiàn)在回過(guò)去看detectHandlerMethods方法,有兩處調(diào)用了getMappingForMethod方法,個(gè)人覺(jué)得這里是可以優(yōu)化的,在第一處判斷method時(shí)否為handler時(shí),創(chuàng)建的RequestMappingInfo對(duì)象可以保存起來(lái),直接拿來(lái)后面使用,就少了一次創(chuàng)建RequestMappingInfo對(duì)象的過(guò)程。然后緊接著進(jìn)入registerHandlerMehtod方法,如下
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
  //創(chuàng)建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。這個(gè)對(duì)象就是封裝的具體Controller下的方法的RequestMapping注解的相關(guān)信息。一個(gè)RequestMapping注解對(duì)應(yīng)一個(gè)RequestMappingInfo對(duì)象。HandlerMethod和RequestMappingInfo類似,是對(duì)Controlelr下具體處理方法的封裝。先看方法的第一行,根據(jù)handler和mehthod創(chuàng)建HandlerMethod對(duì)象。第二行通過(guò)handlerMethods map來(lái)獲取當(dāng)前mapping對(duì)應(yīng)的HandlerMethod。然后判斷是否存在相同的RequestMapping配置。如下這種配置就會(huì)導(dǎo)致此處拋
    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啟動(dòng)(初始化)階段檢查RequestMapping配置是否有歧義,這是其中一處檢查歧義的(后面還會(huì)提到一個(gè)在運(yùn)行時(shí)檢查歧義性的地方)。然后確認(rèn)配置正常以后會(huì)把該RequestMappingInfo和HandlerMethod對(duì)象添加至handlerMethods(LinkedHashMap<RequestMappingInfo,HandlerMethod>)中,靜接著把RequestMapping注解的value和ReuqestMappingInfo對(duì)象添加至urlMap中。
registerHandlerMethod方法簡(jiǎn)單總結(jié)

該方法的主要有3個(gè)職責(zé)

  1. 檢查RequestMapping注解配置是否有歧義。
  2. 構(gòu)建RequestMappingInfo到HandlerMethod的映射map。該map便是AbstractHandlerMethodMapping的成員變量handlerMethods。LinkedHashMap<RequestMappingInfo,HandlerMethod>。
  3. 構(gòu)建AbstractHandlerMethodMapping的成員變量urlMap,MultiValueMap<String,RequestMappingInfo>。這個(gè)數(shù)據(jù)結(jié)構(gòu)可以把它理解成Map<String,List<RequestMappingInfo>>。其中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";
    }
}
  • 初始化完成后,對(duì)應(yīng)AbstractHandlerMethodMapping的urlMap的結(jié)構(gòu)如下


    image.png
  • 以上便是SpringMVC初始化的主要過(guò)程

查找過(guò)程

  • 為了理解查找流程,帶著一個(gè)問(wèn)題來(lái)看,現(xiàn)有如下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";
    }
}
  • 有如下請(qǐng)求


    image.png
  • 這個(gè)請(qǐng)求會(huì)進(jìn)入哪一個(gè)方法?

  • web容器(Tomcat、jetty)接收請(qǐng)求后,交給DispatcherServlet處理。FrameworkServlet調(diào)用對(duì)應(yīng)請(qǐng)求方法(eg:get調(diào)用doGet),然后調(diào)用processRequest方法。進(jìn)入processRequest方法后,一系列處理后,在line:936進(jìn)入doService方法。然后在Line856進(jìn)入doDispatch方法。在line:896獲取當(dāng)前請(qǐng)求的處理器handler。然后進(jìn)入AbstractHandlerMethodMapping的lookupHandlerMethod方法。代碼如下

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
  List<Match> matches = new ArrayList<Match>();
   //根據(jù)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對(duì)應(yīng)的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);
  }
}
  • 進(jìn)入lookupHandlerMethod方法,其中l(wèi)ookupPath="/LookupTest/test1",根據(jù)lookupPath,也就是請(qǐng)求的uri。直接查找urlMap,獲取直接匹配的RequestMappingInfo list。這里會(huì)匹配到3個(gè)RequestMappingInfo。如下


    image.png
  • 然后進(jìn)入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)));
      }
  }
}
  • 這個(gè)方法的職責(zé)是遍歷當(dāng)前請(qǐng)求的uri和mappings中的RequestMappingInfo能否匹配上,如果能匹配上,創(chuàng)建一個(gè)相同的RequestMappingInfo對(duì)象。再獲取RequestMappingInfo對(duì)應(yīng)的handlerMethod。然后創(chuàng)建一個(gè)Match對(duì)象添加至matches list中。執(zhí)行完addMatchingMappings方法,回到lookupHandlerMethod。這時(shí)候matches還有3個(gè)能匹配上的RequestMappingInfo對(duì)象。接下來(lái)的處理便是對(duì)matchers列表進(jìn)行排序,然后獲取列表的第一個(gè)元素作為最佳匹配。返回Match的HandlerMethod。這里進(jìn)入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,看到這里,前面的問(wèn)題就能輕易得出答案了。在value相同的情況,params更能先匹配。所以那個(gè)請(qǐng)求會(huì)進(jìn)入test3()方法。再回到lookupHandlerMethod,在找到HandlerMethod。SpringMVC還會(huì)這里再一次檢查配置的歧義性,這里檢查的原理是通過(guò)比較匹配度最高的兩個(gè)RequestMappingInfo進(jìn)行比較。此處可能會(huì)有疑問(wèn)在初始化SpringMVC有檢查配置的歧義性,這里為什么還會(huì)檢查一次。假如現(xiàn)在Controller中有如下兩個(gè)方法,以下配置是能通過(guò)初始化歧義性檢查的。
@RequestMapping(value = "/test5", method = {RequestMethod.GET, RequestMethod.POST})
@ResponseBody
public String test5(){
    return "method test5";
}
@RequestMapping(value = "/test5", method = {RequestMethod.GET, RequestMethod.DELETE})
@ResponseBody
public String test6(){
    return "method test6";
}
  • 現(xiàn)在執(zhí)行 http://localhost:8080/SpringMVC-Demo/LookupTest/test5 請(qǐng)求,便會(huì)在lookupHandlerMethod方法中拋
    java.lang.IllegalStateException: Ambiguous handler methods mapped for HTTP path 'http://localhost:8080/SpringMVC-Demo/LookupTest/test5'異常。這里拋該異常是因?yàn)镽equestMethodsRequestCondition的compareTo方法是比較的method數(shù)。代碼如下
public int compareTo(RequestMethodsRequestCondition other, HttpServletRequest request) {
  return other.methods.size() - this.methods.size();
}
  • 什么時(shí)候匹配通配符?當(dāng)通過(guò)urlMap獲取不到直接匹配value的RequestMappingInfo時(shí)才會(huì)走通配符匹配進(jìn)入addMatchingMappings方法。

總結(jié)

  • 解析所使用代碼已上傳至github,https://github.com/wycm/SpringMVC-Demo
  • 以上源碼是基于SpringMVC 3.2.2.RELEASE版本。以上便是SpringMVC請(qǐng)求查找的主要過(guò)程,希望對(duì)大家有幫助。本文可能有錯(cuò)誤,希望讀者能夠指出來(lái)。

版權(quán)聲明
作者:wycm
出處:http://www.itdecent.cn/p/08338ea28647

您的支持是對(duì)博主最大的鼓勵(lì),感謝您的認(rèn)真閱讀。
本文版權(quán)歸作者所有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。

歡迎關(guān)注我的個(gè)人公眾號(hào)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容