RequestMappingHandlerMapping請求地址對映流程!

Xianuii發表於2022-12-17

上篇文章裡,我們講解了RequestMappingHandlerMapping請求地址對映的初始化流程,理解了@Controller@RequestMapping是如何被載入到快取中的。

今天我們來進一步學習,在接收到請求時,RequestMappingHandlerMapping是如何進行請求地址對映的。

先放一個類圖,在請求地址對映過程中,會依次執行到這些方法:

講解之前,先總結RequestMappingHandlerMapping的請求地址對映流程:

  1. 獲取handler
    1. 解析request,獲取請求路徑path
    2. 根據path查詢pathLookup快取,獲取路徑匹配的RequestMappingInfo列表
    3. 對上述RequestMappingInfo列表進行篩選,獲取條件匹配的RequestMappingInfo列表
    4. 對上述RequestMappingInfo列表進行排序,獲取匹配度最高的RequestMappingInfo
    5. 根據上述RequestMappingInfo,獲取對應MappingRegistrationHandlerMethod作為handler返回
  2. 建立HandlerExecutionChain物件
  3. 新增配置攔截器
  4. 新增跨域攔截器

1 HandlerMapping

首先,DispatcherServlet會呼叫HandlerMapping介面的getHandler()方法:

HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

這個方法主要起著規範的作用,DispatcherServlet可以根據這個方法呼叫所有HandlerMapping實現類進行請求地址對映。

2 AbstractHandlerMapping

AbstractHandlerMapping是所有HandlerMapping的抽象基類,提供了攔截器、排序和預設處理器等功能。

AbstractHandlerMapping是常見HandlerMapping實現類的共同父類,它的核心功能是定義了獲取HandlerExecutionChain的基礎流程:

  1. 獲取handler(由實現類定義具體邏輯)
  2. 建立HandlerExecutionChain,新增攔截器
  3. 新增跨域攔截器

AbstractHandlerMappinggetHandler()原始碼如下:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {  
// 1、獲取handler
   Object handler = getHandlerInternal(request);  
   if (handler == null) {  
      handler = getDefaultHandler();  
   }  
   if (handler == null) {  
      return null;  
   }  
   // Bean name or resolved handler?  
   if (handler instanceof String) {  
      String handlerName = (String) handler;  
      handler = obtainApplicationContext().getBean(handlerName);  
   }  
  
   // Ensure presence of cached lookupPath for interceptors and others  
   if (!ServletRequestPathUtils.hasCachedPath(request)) {  
      initLookupPath(request);  
   }  
   // 2、建立HandlerExecutionChain,新增攔截器
   HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);  
  
   if (logger.isTraceEnabled()) {  
      logger.trace("Mapped to " + handler);  
   }  
   else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {  
      logger.debug("Mapped to " + executionChain.getHandler());  
   }  
   // 3、新增跨域攔截器
   if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {  
      CorsConfiguration config = getCorsConfiguration(handler, request);  
      if (getCorsConfigurationSource() != null) {  
         CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);  
         config = (globalConfig != null ? globalConfig.combine(config) : config);  
      }  
      if (config != null) {  
         config.validateAllowCredentials();  
      }  
      executionChain = getCorsHandlerExecutionChain(request, executionChain, config);  
   }  
  
   return executionChain;  
}

2.1 獲取handler

AbstractHandlerMapping透過getHandlerInternal()方法獲取handler

該方法由具體實現類進行實現,如果找到匹配的handler,則會返回該handler;如果沒有找到,則會返回null

具體實現我們會在下文的實現類中進行講解。

2.2 建立HandlerExecutionChain,新增攔截器

AbstractHandlerMapping透過getHandlerExecutionChain()方法建立HandlerExecutionChain物件,並新增攔截器。原始碼如下:

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {  
// 1、建立HandlerExecutionChain物件
   HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?  
         (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));  

// 2、新增攔截器
   for (HandlerInterceptor interceptor : this.adaptedInterceptors) {  
      if (interceptor instanceof MappedInterceptor) {  
         MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;  
         if (mappedInterceptor.matches(request)) {  
            chain.addInterceptor(mappedInterceptor.getInterceptor());  
         }  
      }  
      else {  
         chain.addInterceptor(interceptor);  
      }  
   }  
   return chain;  
}

它會對初始化時配置的攔截器進行遍歷:

  1. 如果是MappedInterceptor實現類,會根據匹配規則進行判斷是否新增。
  2. 如果不是MappedInterceptor實現類,會直接新增。

2.3 新增跨域攔截器

新增跨域攔截器分為以下幾個步驟:

  1. 判斷是否存在跨域配置,或是否預檢請求
  2. 獲取handler級別的跨域配置
  3. 獲取HandlerMapping級別的跨域配置
  4. 整合跨域配置
  5. 建立並新增跨域攔截器

2.3.1 判斷是否存在跨域配置

AbstractHandlerMapping中,會判斷handler是否CorsConfigurationSource的實現類(對於RequestMappingHandlerMapping而言,handlerHandlerMethod型別,所以第一個條件永遠是false),以及是否存在HandlerMapping級別的跨域配置源:

protected boolean hasCorsConfigurationSource(Object handler) {  
   if (handler instanceof HandlerExecutionChain) {  
      handler = ((HandlerExecutionChain) handler).getHandler();  
   }  
   return (handler instanceof CorsConfigurationSource || this.corsConfigurationSource != null);  
}

而在AbstractHandlerMethodMapping子抽象類中,會進一步判斷是否存在handler級別(也就是@CrossOrigin級別)的跨域配置:

protected boolean hasCorsConfigurationSource(Object handler) {  
   return super.hasCorsConfigurationSource(handler) ||  
         (handler instanceof HandlerMethod &&  
               this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null);  
}

2.3.2 判斷是否是預檢請求

org.springframework.web.cors.CorsUtils#isPreFlightRequest

public static boolean isPreFlightRequest(HttpServletRequest request) {  
   return (HttpMethod.OPTIONS.matches(request.getMethod()) &&  
         request.getHeader(HttpHeaders.ORIGIN) != null &&  
         request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);  
}

2.3.3 獲取handler級別跨域配置

AbstractHandlerMapping中,會判斷handler是否CorsConfigurationSource的實現類,從中獲取handler級別的跨域配置。對於RequestMappingHandlerMapping而言,handlerHandlerMethod型別,所以第一個條件永遠返回null

protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {  
   Object resolvedHandler = handler;  
   if (handler instanceof HandlerExecutionChain) {  
      resolvedHandler = ((HandlerExecutionChain) handler).getHandler();  
   }  
   if (resolvedHandler instanceof CorsConfigurationSource) {  
      return ((CorsConfigurationSource) resolvedHandler).getCorsConfiguration(request);  
   }  
   return null;  
}

AbstractHandlerMethodMapping子抽象類中,會從mappingRegistryrequest-handler快取)中獲取handler級別的跨域配置(在上篇文章中,我們有講述過RequestMappingHandlerMapping如何快取@CrossOrigin級別的跨域配置的):

protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {  
   CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request);  
   if (handler instanceof HandlerMethod) {  
      HandlerMethod handlerMethod = (HandlerMethod) handler;  
      if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) {  
         return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG;  
      }  
      else {  
         CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod);  
         corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod);  
      }  
   }  
   return corsConfig;  
}

2.3.4 獲取HandlerMapping級別的跨域配置

AbstractHandlerMappingcorsConfigurationSource成員變數中,可以獲取到HandlerMapping級別的跨域配置,該配置可以透過以下方式新增:

@Configuration  
@EnableWebMvc  
public class WebMvcConfig implements WebMvcConfigurer {  
    @Override  
    public void addCorsMappings(CorsRegistry registry) {
	    // 新增HandlerMapping級別的跨域配置
    }
}

2.3.5 整合跨域配置

在整合跨域配置過程中,有三種情況:

  1. 對於originsoriginPatternsallowedHeadersexposedHeadersmethods等列表屬性,會獲取全部。
  2. 對於allowCredentials,會優先獲取方法級別的配置。
  3. 對於maxAge,會獲取最大值。

具體可以檢視相關原始碼:

public CorsConfiguration combine(@Nullable CorsConfiguration other) {  
   if (other == null) {  
      return this;  
   }  
   // Bypass setAllowedOrigins to avoid re-compiling patterns  
   CorsConfiguration config = new CorsConfiguration(this);  
   List<String> origins = combine(getAllowedOrigins(), other.getAllowedOrigins());  
   List<OriginPattern> patterns = combinePatterns(this.allowedOriginPatterns, other.allowedOriginPatterns);  
   config.allowedOrigins = (origins == DEFAULT_PERMIT_ALL && !CollectionUtils.isEmpty(patterns) ? null : origins);  
   config.allowedOriginPatterns = patterns;  
   config.setAllowedMethods(combine(getAllowedMethods(), other.getAllowedMethods()));  
   config.setAllowedHeaders(combine(getAllowedHeaders(), other.getAllowedHeaders()));  
   config.setExposedHeaders(combine(getExposedHeaders(), other.getExposedHeaders()));  
   Boolean allowCredentials = other.getAllowCredentials();  
   if (allowCredentials != null) {  
      config.setAllowCredentials(allowCredentials);  
   }  
   Long maxAge = other.getMaxAge();  
   if (maxAge != null) {  
      config.setMaxAge(maxAge);  
   }  
   return config;  
}

2.3.6 建立並新增跨域攔截器

在這一步,對於預檢請求,會建立HandlerExecutionChain;對於普通請求,會建立CorsInterceptor攔截器,並新增到首位:

protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,  
      HandlerExecutionChain chain, @Nullable CorsConfiguration config) {  
  
   if (CorsUtils.isPreFlightRequest(request)) {  
      HandlerInterceptor[] interceptors = chain.getInterceptors();  
      return new HandlerExecutionChain(new PreFlightHandler(config), interceptors);  
   }  
   else {  
      chain.addInterceptor(0, new CorsInterceptor(config));  
      return chain;  
   }  
}

3 AbstractHandlerMethodMapping

AbstractHandlerMethodMappingHandlerMethod請求對映的抽象基類,它的getHandlerInternal()方法定義了請求地址對映的核心流程:

  1. 解析請求路徑
  2. 根據請求地址查詢HandlerMethod

AbstractHandlerMethodMapping#getHandlerInternal

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {  
// 1、解析請求地址
   String lookupPath = initLookupPath(request);  
   this.mappingRegistry.acquireReadLock();  
   try {  
   // 2、根據請求地址查詢HandlerMethod
      HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);  
      return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);  
   }  
   finally {  
      this.mappingRegistry.releaseReadLock();  
   }  
}

3.1 解析請求路徑

解析請求路徑過程會獲取當前請求的介面地址路徑。

簡單來說,會去除請求地址開頭的contextPaht。例如在application.properties配置contextPath如下:

server.servlet.context-path=/context-path

此時,請求/context-path/test地址,經過initLookPath()方法處理,會返回/test為實際請求路徑。

實際上,這也很容易理解。因為在RequestMappingHandlerMapping初始化pathLookup對映快取時,就沒有將contextPath考慮在內,那麼在實際處理請求時,當然也要把contextPath去掉。

解析請求路徑的作用也是為了方便直接從pathLookup對映快取中獲取對應的RequestMappingInfo資訊。

AbstractHandlerMapping#initLookupPath原始碼如下:

protected String initLookupPath(HttpServletRequest request) {  
   if (usesPathPatterns()) {  
      request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);  
      RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);  
      String lookupPath = requestPath.pathWithinApplication().value();  
      return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);  
   }  
   else {  
      return getUrlPathHelper().resolveAndCacheLookupPath(request);  
   }  
}

3.2 根據請求路徑查詢HandlerMethod

AbstractHandlerMethodMapping#lookupHandlerMethod方法中,會按如下步驟獲取HandlerMethod

  1. 根據請求路徑從pathLookup對映快取查詢對應的RequestMappingInfo列表。
  2. 根據RequestMappingInforegistry快取中獲取對應的MappingRegistration列表。
  3. 根據當前request,對MappingRegistration列表按匹配度進行排序。
  4. 從中取匹配度最高的HandlerMethod進行返回。

AbstractHandlerMethodMapping#lookupHandlerMethod原始碼如下:

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {  
   List<Match> matches = new ArrayList<>();  
   List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);  
   if (directPathMatches != null) {  
      addMatchingMappings(directPathMatches, matches, request);  
   }  
   if (matches.isEmpty()) {  
      addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);  
   }  
   if (!matches.isEmpty()) {  
      Match bestMatch = matches.get(0);  
      if (matches.size() > 1) {  
         Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));  
         matches.sort(comparator);  
         bestMatch = matches.get(0);  
         if (logger.isTraceEnabled()) {  
            logger.trace(matches.size() + " matching mappings: " + matches);  
         }  
         if (CorsUtils.isPreFlightRequest(request)) {  
            for (Match match : matches) {  
               if (match.hasCorsConfig()) {  
                  return PREFLIGHT_AMBIGUOUS_MATCH;  
               }  
            }  
         }  
         else {  
            Match secondBestMatch = matches.get(1);  
            if (comparator.compare(bestMatch, secondBestMatch) == 0) {  
               Method m1 = bestMatch.getHandlerMethod().getMethod();  
               Method m2 = secondBestMatch.getHandlerMethod().getMethod();  
               String uri = request.getRequestURI();  
               throw new IllegalStateException(  
                     "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");  
            }  
         }  
      }  
      request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());  
      handleMatch(bestMatch.mapping, lookupPath, request);  
      return bestMatch.getHandlerMethod();  
   }  
   else {  
      return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);  
   }  
}

3.2.1 查詢pathLookup快取

RequestMappingHandlerMapping請求地址對映的初始化過程中,會將@RequestMapping中的資訊快取到pathLookup中,其中該註解的請求路徑作為key,該註解的各屬性封裝成RequestMappingInfo作為值。

需要注意的是,pathLookup的型別是MultiValueMap<String, T>,這裡的T就是RequestMappingInfo

pathLookup的底層資料結構實際上是path-List<RequestMappingInfo>,這是因為請求路徑不是介面的唯一指標,還包括請求頭、請求方法等資訊。

所以,一個請求地址實際上可能對映著多個HandlerMethod

例如,我們可以定義如下介面:

@RestController
public class SamePathController {
	@GetMapping("/samePath")
	public String get() {
		return "get";
	}
	@PostMapping("/samePath")
	public String post() {
		return "post";
	}
}

此時,GET localhost:8080/samePathPOST localhost:8080/samePath可以分別請求到對應的介面。

回到AbstractHandlerMethodMapping#getHandlerInternal原始碼,此時透過請求路徑可以獲取多個RequestMappingInfo

List<RequestMappingInfo> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);

3.2.2 查詢registry快取

RequestMappingHandlerMapping請求地址對映的初始化過程中,會將介面的詳細資訊快取到registry中,將上述RequestMappingInfo作為key,將RequestMappingInfoHanlderMethod等資訊裝成MappingRegistration作為值。

registry的型別是Map<T, MappingRegistration<T>>,這裡的T指的是RequestMappingInfo

需要注意的是,由於RequestMappingInfo根據介面的@RequestMapping資訊進行構造,如果存在@RequestMapping資訊完全相同的多個介面,專案是無法啟動的。

因此,RequestMappingInfo可以唯一定位到該介面,即RequestMappingInfoMappingRegistration是一一對應的。我們也可以將RequestMappingInfo等效於實際介面。

我們可以總結一下pathLookupregistry快取的關係:

回到AbstractHandlerMethodMapping#getHandlerInternal原始碼:

if (directPathMatches != null) {  
   addMatchingMappings(directPathMatches, matches, request);  
}  
if (matches.isEmpty()) {  
   addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);  
}

存在兩種情況:

  1. 如果在pathLookup快取中找到對應List<RequestMappingInfo>,會進一步從該列表中查詢更加匹配的RequestMappingInfo,並根據該RequestMappingregistry快取中找到對應的MappingRegistration,封裝成Match物件返回。
  2. 如果在pathLookup快取中沒有找到對應List<RequestMappingInfo>,會遍歷registry快取中的所有key,從中查詢更加匹配的RequestMappingInfo,並根據該RequestMappingregistry快取中找到對應的MappingRegistration,封裝成Match物件返回。

具體流程對應的AbstractHandlerMethodMapping#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, this.mappingRegistry.getRegistrations().get(mapping)));  
      }  
   }  
}

查詢更加匹配的RequestMappingInfo對應的是RequestMappingInfoHandlerMapping#getMatchingMapping方法:

protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {  
   return info.getMatchingCondition(request);  
}

RequestMappingInfo#getMatchingCondition方法會對請求的methodsparamsconsumesproduces以及path進行校驗,只有所有條件透過才會返回該RequestMappingInfo,否則會返回null。具體原始碼如下:

public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {  
   RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);  
   if (methods == null) {  
      return null;  
   }  
   ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);  
   if (params == null) {  
      return null;  
   }  
   HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);  
   if (headers == null) {  
      return null;  
   }  
   ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);  
   if (consumes == null) {  
      return null;  
   }  
   ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);  
   if (produces == null) {  
      return null;  
   }  
   PathPatternsRequestCondition pathPatterns = null;  
   if (this.pathPatternsCondition != null) {  
      pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);  
      if (pathPatterns == null) {  
         return null;  
      }  
   }  
   PatternsRequestCondition patterns = null;  
   if (this.patternsCondition != null) {  
      patterns = this.patternsCondition.getMatchingCondition(request);  
      if (patterns == null) {  
         return null;  
      }  
   }  
   RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);  
   if (custom == null) {  
      return null;  
   }  
   return new RequestMappingInfo(this.name, pathPatterns, patterns,  
         methods, params, headers, consumes, produces, custom, this.options);  
}

通常情況下,透過這種判斷可以篩選出唯一一個對應的RequestMappingInfo,除非是我們定義的介面比較特殊。

例如,我們定義介面如下:

@RestController
public class SamePathController {
	@RequestMapping(value = "samePath", method = {RequestMethod.GET, RequestMethod.POST})
	public String getAndPost() {
		return "getAndPost";
	}
	@PostMapping("/samePath")
	public String post() {
		return "post";
	}
}

此時,請求GET localhost:8080/samePath,可以篩選出來唯一一個定位到getAndPost()介面的RequestMappingInfo;請求POST localhost:8080/samePath,值可以篩選出兩個分別定義到getAndPost()post()方法的RequestMappingInfo,因為它們的規則都滿足條件,需要進一步篩選。

3.2.3 按匹配度排序

通常情況下,透過上述步驟可以篩選出唯一一個RequestMappingInfo

但是也有可能定義出條件重疊的介面(不推薦),此時會篩選出多個RequestMappingInfo。此時,需要根據某種規則進行匹配度排序。

RequestMappingInfo對於匹配度排序的規則是:

  1. 比較methodsparamsheaders等條件的長度:越短越具體,匹配度越高。
  2. 長度相等時,比較其他特殊規則:例如methods包含HEAD方法的匹配度高。

具體實現原始碼在RequestMappingInfo#compareTo

public int compareTo(RequestMappingInfo other, HttpServletRequest request) {  
   int result;  
   // Automatic vs explicit HTTP HEAD mapping  
   if (HttpMethod.HEAD.matches(request.getMethod())) {  
      result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);  
      if (result != 0) {  
         return result;  
      }  
   }  
   result = getActivePatternsCondition().compareTo(other.getActivePatternsCondition(), request);  
   if (result != 0) {  
      return result;  
   }  
   result = this.paramsCondition.compareTo(other.getParamsCondition(), request);  
   if (result != 0) {  
      return result;  
   }  
   result = this.headersCondition.compareTo(other.getHeadersCondition(), request);  
   if (result != 0) {  
      return result;  
   }  
   result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);  
   if (result != 0) {  
      return result;  
   }  
   result = this.producesCondition.compareTo(other.getProducesCondition(), request);  
   if (result != 0) {  
      return result;  
   }  
   // Implicit (no method) vs explicit HTTP method mappings  
   result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);  
   if (result != 0) {  
      return result;  
   }  
   result = this.customConditionHolder.compareTo(other.customConditionHolder, request);  
   if (result != 0) {  
      return result;  
   }  
   return 0;  
}

3.2.4 獲取匹配度最高的HandlerMethod

透過上述步驟,我們最終獲取到匹配度最高的RequestMappingInfo,直接取對應MappingRegistrationHandlerMethod成員變數返回即可。

相關文章