SpringMVC 九大組件之 HandlerMapping 深入分析

松哥原創的 Spring Boot 視頻教程已經殺青,感興趣的小夥伴戳這裏-->Spring Boot+Vue+微人事視頻教程


前面跟小夥伴們分享了 SpringMVC 一個大致的初始化流程以及請求的大致處理流程,在請求處理過程中,涉及到九大組件,分別是:

  1. HandlerMapping
  2. HandlerAdapter
  3. HandlerExceptionResolver
  4. ViewResolver
  5. RequestToViewNameTranslator
  6. LocaleResolver
  7. ThemeResolver
  8. MultipartResolver
  9. FlashMapManager

這些組件相信小夥伴們在日常開發中多多少少都有涉及到,如果你對這些組件感到陌生,可以在公衆號後臺回覆 ssm,免費獲取松哥的入門視頻教程。

那麼接下來的幾篇文章,松哥想和大家深入分析這九大組件,從用法到源碼,挨個分析,今天我們就先來看看這九大組件中的第一個 HandlerMapping。

1.概覽

HandlerMapping 叫做處理器映射器,它的作用就是根據當前 request 找到對應的 Handler 和 Interceptor,然後封裝成一個 HandlerExecutionChain 對象返回,我們來看下 HandlerMapping 接口:

public interface HandlerMapping {
 String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingHandler";
 @Deprecated
 String LOOKUP_PATH = HandlerMapping.class.getName() + ".lookupPath";
 String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
 String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";
 String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + ".introspectTypeLevelMapping";
 String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
 String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables";
 String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
 default boolean usesPathPatterns() {
  return false;
 }
 @Nullable
 HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

可以看到,除了一堆聲明的常量外,其實就一個需要實現的方法 getHandler,該方法的返回值就是我們所瞭解到的 HandlerExecutionChain。

HandlerMapping 的繼承關係如下:

這個繼承關係雖然看着有點繞,其實仔細觀察就兩大類:

  • AbstractHandlerMethodMapping
  • AbstractUrlHandlerMapping

其他的都是一些輔助接口。

AbstractHandlerMethodMapping 體系下的都是根據方法名進行匹配的,而 AbstractUrlHandlerMapping 體系下的都是根據 URL 路徑進行匹配的,這兩者有一個共同的父類 AbstractHandlerMapping,接下來我們就對這三個關鍵類進行詳細分析。

2.AbstractHandlerMapping

AbstractHandlerMapping 實現了 HandlerMapping 接口,無論是通過 URL 進行匹配還是通過方法名進行匹配,都是通過繼承 AbstractHandlerMapping 來實現的,所以 AbstractHandlerMapping 所做的事情其實就是一些公共的事情,將以一些需要具體處理的事情則交給子類去處理,這其實就是典型的模版方法模式。

AbstractHandlerMapping 間接繼承自 ApplicationObjectSupport,並重寫了 initApplicationContext 方法(其實該方法也是一個模版方法),這也是 AbstractHandlerMapping 的初始化入口方法,我們一起來看下:

@Override
protected void initApplicationContext() throws BeansException {
 extendInterceptors(this.interceptors);
 detectMappedInterceptors(this.adaptedInterceptors);
 initInterceptors();
}

三個方法都和攔截器有關。

extendInterceptors

protected void extendInterceptors(List<Object> interceptors) {
}

extendInterceptors 是一個模版方法,可以在子類中實現,子類實現了該方法之後,可以對攔截器進行添加、刪除或者修改,不過在 SpringMVC 的具體實現中,其實這個方法並沒有在子類中進行實現。

detectMappedInterceptors

protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
 mappedInterceptors.addAll(BeanFactoryUtils.beansOfTypeIncludingAncestors(
   obtainApplicationContext(), MappedInterceptor.classtruefalse).values());
}

detectMappedInterceptors 方法會從 SpringMVC 容器以及 Spring 容器中查找所有 MappedInterceptor 類型的 Bean,查找到之後添加到 mappedInterceptors 屬性中(其實就是全局的 adaptedInterceptors 屬性)。一般來說,我們定義好一個攔截器之後,還要在 XML 文件中配置該攔截器,攔截器以及各種配置信息,最終就會被封裝成一個 MappedInterceptor 對象。

initInterceptors

protected void initInterceptors() {
 if (!this.interceptors.isEmpty()) {
  for (int i = 0; i < this.interceptors.size(); i++) {
   Object interceptor = this.interceptors.get(i);
   if (interceptor == null) {
    throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
   }
   this.adaptedInterceptors.add(adaptInterceptor(interceptor));
  }
 }
}

initInterceptors 方法主要是進行攔截器的初始化操作,具體內容是將 interceptors 集合中的攔截器添加到 adaptedInterceptors 集合中。

至此,我們看到,所有攔截器最終都會被存入 adaptedInterceptors 變量中。

AbstractHandlerMapping 的初始化其實也就是攔截器的初始化過程。

爲什麼 AbstractHandlerMapping 中對攔截器如此重視呢?其實不是重視,大家想想,AbstractUrlHandlerMapping 和 AbstractHandlerMethodMapping 最大的區別在於查找處理器的區別,一旦處理器找到了,再去找攔截器,但是攔截器都是統一的,並沒有什麼明顯區別,所以攔截器就統一在 AbstractHandlerMapping 中進行處理,而不會去 AbstractUrlHandlerMapping 或者 AbstractHandlerMethodMapping 中處理。

接下來我們再來看看 AbstractHandlerMapping#getHandler 方法,看看處理器是如何獲取到的:

@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
 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);
 }
 HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
 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;
}

這個方法的執行流程是這樣的:

  1. 首先調用 getHandlerInternal 方法去嘗試獲取處理器,getHandlerInternal 方法也是一個模版方法,該方法將在子類中實現。
  2. 如果沒找到相應的處理器,則調用 getDefaultHandler 方法獲取默認的處理器,我們在配置 HandlerMapping 的時候可以配置默認的處理器。
  3. 如果找到的處理器是一個字符串,則根據該字符串找去 SpringMVC 容器中找到對應的 Bean。
  4. 確保 lookupPath 存在,一會找對應的攔截器的時候會用到。
  5. 找到 handler 之後,接下來再調用 getHandlerExecutionChain 方法獲取 HandlerExecutionChain 對象。
  6. 接下來 if 裏邊的是進行跨域處理的,獲取到跨域的相關配置,然後進行驗證&配置,檢查是否允許跨域。跨域這塊的配置以及校驗還是蠻有意思的,松哥以後專門寫文章來和小夥伴們細聊。

接下來我們再來看看第五步的 getHandlerExecutionChain 方法的執行邏輯,正是在這個方法裏邊把 handler 變成了 HandlerExecutionChain:

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
 HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
   (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
 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;
}

這裏直接根據已有的 handler 創建一個新的 HandlerExecutionChain 對象,然後遍歷 adaptedInterceptors 集合,該集合裏存放的都是攔截器,如果攔截器的類型是 MappedInterceptor,則調用 matches 方法去匹配一下,看一下是否是攔截當前請求的攔截器,如果是,則調用 chain.addInterceptor 方法加入到 HandlerExecutionChain 對象中;如果就是一個普通攔截器,則直接加入到 HandlerExecutionChain 對象中。

這就是 AbstractHandlerMapping#getHandler 方法的大致邏輯,可以看到,這裏留了一個模版方法 getHandlerInternal 在子類中實現,接下來我們就來看看它的子類。

3.AbstractUrlHandlerMapping

AbstractUrlHandlerMapping,看名字就知道,都是按照 URL 地址來進行匹配的,它的原理就是將 URL 地址與對應的 Handler 保存在同一個 Map 中,當調用 getHandlerInternal 方法時,就根據請求的 URL 去 Map 中找到對應的 Handler 返回就行了。

這裏我們就先從他的 getHandlerInternal 方法開始看起:

@Override
@Nullable
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
 String lookupPath = initLookupPath(request);
 Object handler;
 if (usesPathPatterns()) {
  RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request);
  handler = lookupHandler(path, lookupPath, request);
 }
 else {
  handler = lookupHandler(lookupPath, request);
 }
 if (handler == null) {
  // We need to care for the default handler directly, since we need to
  // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
  Object rawHandler = null;
  if (StringUtils.matchesCharacter(lookupPath, '/')) {
   rawHandler = getRootHandler();
  }
  if (rawHandler == null) {
   rawHandler = getDefaultHandler();
  }
  if (rawHandler != null) {
   // Bean name or resolved handler?
   if (rawHandler instanceof String) {
    String handlerName = (String) rawHandler;
    rawHandler = obtainApplicationContext().getBean(handlerName);
   }
   validateHandler(rawHandler, request);
   handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
  }
 }
 return handler;
}
  1. 首先找到 lookupPath,就是請求的路徑。這個方法本身松哥就不多說了,之前在Spring5 裏邊的新玩法!這種 URL 請求讓我漲見識了!一文中有過介紹。
  2. 接下來就是調用 lookupHandler 方法獲取 Handler 對象,lookupHandler 有一個重載方法,具體用哪個,主要看所使用的 URL 匹配模式,如果使用了最新的 PathPattern(Spring5 之後的),則使用三個參數的 lookupHandler;如果還是使用之前舊的 AntPathMatcher,則這裏使用兩個參數的 lookupHandler。
  3. 如果前面沒有獲取到 handler 實例,則接下來再做各種嘗試,去分別查找 RootHandler、DefaultHandler 等,如果找到的 Handler 是一個 String,則去 Spring 容器中查找該 String 對應的 Bean,再調用 validateHandler 方法來校驗找到的 handler 和 request 是否匹配,不過這是一個空方法,子類也沒有實現,所以可以忽略之。最後再通過 buildPathExposingHandler 方法給找到的 handler 添加一些參數。

這就是整個 getHandlerInternal 方法的邏輯,實際上並不難,裏邊主要涉及到 lookupHandler 和 buildPathExposingHandler 兩個方法,需要和大家詳細介紹下,我們分別來看。

lookupHandler

lookupHandler 有兩個,我們分別來看。

@Nullable
protected Object lookupHandler(String lookupPath, HttpServletRequest request) throws Exception {
 Object handler = getDirectMatch(lookupPath, request);
 if (handler != null) {
  return handler;
 }
 // Pattern match?
 List<String> matchingPatterns = new ArrayList<>();
 for (String registeredPattern : this.handlerMap.keySet()) {
  if (getPathMatcher().match(registeredPattern, lookupPath)) {
   matchingPatterns.add(registeredPattern);
  }
  else if (useTrailingSlashMatch()) {
   if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", lookupPath)) {
    matchingPatterns.add(registeredPattern + "/");
   }
  }
 }
 String bestMatch = null;
 Comparator<String> patternComparator = getPathMatcher().getPatternComparator(lookupPath);
 if (!matchingPatterns.isEmpty()) {
  matchingPatterns.sort(patternComparator);
  bestMatch = matchingPatterns.get(0);
 }
 if (bestMatch != null) {
  handler = this.handlerMap.get(bestMatch);
  if (handler == null) {
   if (bestMatch.endsWith("/")) {
    handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
   }
   if (handler == null) {
    throw new IllegalStateException(
      "Could not find handler for best pattern match [" + bestMatch + "]");
   }
  }
  // Bean name or resolved handler?
  if (handler instanceof String) {
   String handlerName = (String) handler;
   handler = obtainApplicationContext().getBean(handlerName);
  }
  validateHandler(handler, request);
  String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, lookupPath);
  // There might be multiple 'best patterns', let's make sure we have the correct URI template variables
  // for all of them
  Map<String, String> uriTemplateVariables = new LinkedHashMap<>();
  for (String matchingPattern : matchingPatterns) {
   if (patternComparator.compare(bestMatch, matchingPattern) == 0) {
    Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, lookupPath);
    Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
    uriTemplateVariables.putAll(decodedVars);
   }
  }
  return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables);
 }
 // No handler found...
 return null;
}
@Nullable
private Object getDirectMatch(String urlPath, HttpServletRequest request) throws Exception {
 Object handler = this.handlerMap.get(urlPath);
 if (handler != null) {
  // Bean name or resolved handler?
  if (handler instanceof String) {
   String handlerName = (String) handler;
   handler = obtainApplicationContext().getBean(handlerName);
  }
  validateHandler(handler, request);
  return buildPathExposingHandler(handler, urlPath, urlPath, null);
 }
 return null;
}
  1. 這裏首先調用 getDirectMatch 方法直接去 handlerMap 中找對應的處理器,handlerMap 中就保存了請求 URL 和處理器的映射關係,具體的查找過程就是先去 handlerMap 中找,找到了,如果是 String,則去 Spring 容器中找對應的 Bean,然後調用 validateHandler 方法去驗證(實際上沒有驗證,前面已經說了),最後調用 buildPathExposingHandler 方法添加攔截器。
  2. 如果 getDirectMatch 方法返回值不爲 null,則直接將查找到的 handler 返回,方法到此爲止。那麼什麼情況下 getDirectMatch 方法的返回值不爲 null 呢?簡單來收就是沒有使用通配符的情況下,請求地址中沒有通配符,一個請求地址對應一個處理器,只有這種情況,getDirectMatch 方法返回值纔不爲 null,因爲 handlerMap 中保存的是代碼的定義,比如我們定義代碼的時候,某個處理器的訪問路徑可能帶有通配符,但是當我們真正發起請求的時候,請求路徑裏是沒有通配符的,這個時候再去 handlerMap 中就找不對對應的處理器了。如果用到了定義接口時用到了通配符,則需要在下面的代碼中繼續處理。
  3. 接下來處理通配符的情況。首先定義 matchingPatterns 集合,將當前請求路徑和 handlerMap 集合中保存的請求路徑規則進行對比,凡是能匹配上的規則都直接存入 matchingPatterns 集合中。具體處理中,還有一個 useTrailingSlashMatch 的可能,有的小夥伴 SpringMVC 用的不熟練,看到這裏可能就懵了,這裏是這樣的,SpringMVC 中,默認是可以匹配結尾 / 的,舉個簡單例子,如果你定義的接口是 /user,那麼請求路徑可以是 /user 也可以 /user/,這兩種默認都是支持的,所以這裏的 useTrailingSlashMatch 分支主要是處理後面這種情況,處理方式很簡單,就在 registeredPattern 後面加上 / 然後繼續和請求路徑進行匹配。
  4. 由於一個請求 URL 可能會和定義的多個接口匹配上,所以 matchingPatterns 變量是一個數組,接下來就要對 matchingPatterns 進行排序,排序完成後,選擇排序後的第一項作爲最佳選項賦值給 bestMatch 變量。默認的排序規則是 AntPatternComparator,當然開發者也可以自定義。AntPatternComparator 中定義的優先級如下:
路由配置 優先級
不含任何特殊符號的路徑,如:配置路由/a/b/c 第一優先級
帶有{}的路徑,如:/a/{b}/c 第二優先級
帶有正則的路徑,如:/a/{regex:\d{3}}/c 第三優先級
帶有*的路徑,如:/a/b/* 第四優先級
帶有**的路徑,如:/a/b/** 第五優先級
最模糊的匹配:/** 最低優先級
  1. 找到 bestMatch 之後,接下來再根據 bestMatch 去 handlerMap 中找到對應的處理器,直接找如果沒找到,就去檢查 bestMatch 是否以 / 結尾,如果是以 / 結尾,則去掉結尾的 / 再去 handlerMap 中查找,如果還沒找到,那就該拋異常出來了。如果找到的 handler 是 String 類型的,則再去 Spring 容器中查找對應的 Bean,接下來再調用 validateHandler 方法進行驗證。
  2. 接下來調用 extractPathWithinPattern 方法提取出映射路徑,例如定義的接口規則是 myroot/*.html,請求路徑是 myroot/myfile.html,那麼最終獲取到的就是 myfile.html
  3. 接下來的 for 循環是爲了處理存在多個最佳匹配規則的情況,在第四步中,我們對 matchingPatterns 進行排序,排序完成後,選擇第一項作爲最佳選項賦值給 bestMatch,但是最佳選項可能會有多個,這裏就是處理最佳選項有多個的情況。
  4. 最後調用 buildPathExposingHandler 方法註冊兩個內部攔截器,該方法下文我會給大家詳細介紹。

lookupHandler 還有一個重載方法,不過只要大家把這個方法的執行流程搞清楚了,重載方法其實很好理解,這裏松哥就不再贅述了,唯一要說的就是重載方法用了 PathPattern 去匹配 URL 路徑,而這個方法用了 AntPathMatcher 去匹配 URL 路徑。

buildPathExposingHandler

protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern,
  String pathWithinMapping, @Nullable Map<String, String> uriTemplateVariables)
 
{
 HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler);
 chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping));
 if (!CollectionUtils.isEmpty(uriTemplateVariables)) {
  chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables));
 }
 return chain;
}

buildPathExposingHandler 方法向 HandlerExecutionChain 中添加了兩個攔截器 PathExposingHandlerInterceptor 和 UriTemplateVariablesHandlerInterceptor,這兩個攔截器在各自的 preHandle 中分別向 request 對象添加了一些屬性,具體添加的屬性小夥伴們可以自行查看,這個比較簡單,我就不多說了。

在前面的方法中,涉及到一個重要的變量 handlerMap,我們定義的接口和處理器之間的關係都保存在這個變量中,那麼這個變量是怎麼初始化的呢?這就涉及到 AbstractUrlHandlerMapping 中的另一個方法 registerHandler:

protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
 for (String urlPath : urlPaths) {
  registerHandler(urlPath, beanName);
 }
}
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
 Object resolvedHandler = handler;
 if (!this.lazyInitHandlers && handler instanceof String) {
  String handlerName = (String) handler;
  ApplicationContext applicationContext = obtainApplicationContext();
  if (applicationContext.isSingleton(handlerName)) {
   resolvedHandler = applicationContext.getBean(handlerName);
  }
 }
 Object mappedHandler = this.handlerMap.get(urlPath);
 if (mappedHandler != null) {
  if (mappedHandler != resolvedHandler) {
   throw new IllegalStateException(
     "Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
     "]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
  }
 }
 else {
  if (urlPath.equals("/")) {
   setRootHandler(resolvedHandler);
  }
  else if (urlPath.equals("/*")) {
   setDefaultHandler(resolvedHandler);
  }
  else {
   this.handlerMap.put(urlPath, resolvedHandler);
   if (getPatternParser() != null) {
    this.pathPatternHandlerMap.put(getPatternParser().parse(urlPath), resolvedHandler);
   }
  }
 }
}

registerHandler(String[],String) 方法有兩個參數,第一個就是定義的請求路徑,第二個參數則是處理器 Bean 的名字,第一個參數是一個數組,那是因爲同一個處理器可以對應多個不同的請求路徑。

在重載方法 registerHandler(String,String) 裏邊,完成了 handlerMap 的初始化,具體流程如下:

  1. 如果沒有設置 lazyInitHandlers,並且 handler 是 String 類型,那麼就去 Spring 容器中找到對應的 Bean 賦值給 resolvedHandler。
  2. 根據 urlPath 去 handlerMap 中查看是否已經有對應的處理器了,如果有的話,則拋出異常,一個 URL 地址只能對應一個處理器,這個很好理解。
  3. 接下來根據 URL 路徑,將處理器進行配置,最終添加到 handlerMap 變量中。

這就是 AbstractUrlHandlerMapping 的主要工作,其中 registerHandler 將在它的子類中調用。

接下來我們來看 AbstractUrlHandlerMapping 的子類。

3.1 SimpleUrlHandlerMapping

爲了方便處理,SimpleUrlHandlerMapping 中自己定義了一個 urlMap 變量,這樣可以在註冊之前做一些預處理,例如確保所有的 URL 都是以 / 開始。SimpleUrlHandlerMapping 在定義時重寫了父類的 initApplicationContext 方法,並在該方法中調用了 registerHandlers,在 registerHandlers 中又調用了父類的 registerHandler 方法完成了 handlerMap 的初始化操作:

@Override
public void initApplicationContext() throws BeansException {
 super.initApplicationContext();
 registerHandlers(this.urlMap);
}
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
 if (urlMap.isEmpty()) {
  logger.trace("No patterns in " + formatMappingName());
 }
 else {
  urlMap.forEach((url, handler) -> {
   // Prepend with slash if not already present.
   if (!url.startsWith("/")) {
    url = "/" + url;
   }
   // Remove whitespace from handler bean name.
   if (handler instanceof String) {
    handler = ((String) handler).trim();
   }
   registerHandler(url, handler);
  });
 }
}

這塊代碼很簡單,實在沒啥好說的,如果 URL 不是以 / 開頭,則手動給它加上 / 即可。有小夥伴們可能要問了,urlMap 的值從哪裏來?當然是從我們的配置文件裏邊來呀,像下面這樣:

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="urlMap">
        <map>
            <entry key="/aaa" value-ref="/hello"/>
        </map>
    </property>
</bean>

3.2 AbstractDetectingUrlHandlerMapping

AbstractDetectingUrlHandlerMapping 也是 AbstractUrlHandlerMapping 的子類,但是它和 SimpleUrlHandlerMapping 有一些不一樣的地方。

不一樣的是哪裏呢?

AbstractDetectingUrlHandlerMapping 會自動查找到 SpringMVC 容器以及 Spring 容器中的所有 beanName,然後根據 beanName 解析出對應的 URL 地址,再將解析出的 url 地址和對應的 beanName 註冊到父類的 handlerMap 變量中。換句話說,如果你用了 AbstractDetectingUrlHandlerMapping,就不用像 SimpleUrlHandlerMapping 那樣去挨個配置 URL 地址和處理器的映射關係了。我們來看下 AbstractDetectingUrlHandlerMapping#initApplicationContext 方法:

@Override
public void initApplicationContext() throws ApplicationContextException {
 super.initApplicationContext();
 detectHandlers();
}
protected void detectHandlers() throws BeansException {
 ApplicationContext applicationContext = obtainApplicationContext();
 String[] beanNames = (this.detectHandlersInAncestorContexts ?
   BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
   applicationContext.getBeanNamesForType(Object.class))
;
 for (String beanName : beanNames) {
  String[] urls = determineUrlsForHandler(beanName);
  if (!ObjectUtils.isEmpty(urls)) {
   registerHandler(urls, beanName);
  }
 }
}

AbstractDetectingUrlHandlerMapping 重寫了父類的 initApplicationContext 方法,並在該方法中調用了 detectHandlers 方法,在 detectHandlers 中,首先查找到所有的 beanName,然後調用 determineUrlsForHandler 方法分析出 beanName 對應的 URL,不過這裏的 determineUrlsForHandler 方法是一個空方法,具體的實現在它的子類中,AbstractDetectingUrlHandlerMapping 只有一個子類 BeanNameUrlHandlerMapping,我們一起來看下:

public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
 @Override
 protected String[] determineUrlsForHandler(String beanName) {
  List<String> urls = new ArrayList<>();
  if (beanName.startsWith("/")) {
   urls.add(beanName);
  }
  String[] aliases = obtainApplicationContext().getAliases(beanName);
  for (String alias : aliases) {
   if (alias.startsWith("/")) {
    urls.add(alias);
   }
  }
  return StringUtils.toStringArray(urls);
 }

}

這個類很簡單,裏邊就一個 determineUrlsForHandler 方法,這個方法的執行邏輯也很簡單,就判斷 beanName 是不是以 / 開始,如果是,則將之作爲 URL。

如果我們想要在項目中使用 BeanNameUrlHandlerMapping,配置方式如下:

<bean class="org.javaboy.init.HelloController" name="/hello"/>
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
</bean>

注意,Controller 的 name 必須是以 / 開始,否則該 bean 不會被自動作爲處理器。

至此,AbstractUrlHandlerMapping 體系下的東西就和大家分享完了。

4.AbstractHandlerMethodMapping

AbstractHandlerMethodMapping 體系下只有三個類,分別是 AbstractHandlerMethodMapping、RequestMappingInfoHandlerMapping 以及 RequestMappingHandlerMapping,如下圖:

在前面第三小節的 AbstractUrlHandlerMapping 體系下,一個 Handler 一般就是一個類,但是在 AbstractHandlerMethodMapping 體系下,一個 Handler 就是一個 Mehtod,這也是我們目前使用 SpringMVC 時最常見的用法,即直接用 @RequestMapping 去標記一個方法,該方法就是一個 Handler。

接下來我們就一起來看看 AbstractHandlerMethodMapping。

4.1 初始化流程

AbstractHandlerMethodMapping 類實現了 InitializingBean 接口,所以 Spring 容器會自動調用其 afterPropertiesSet 方法,在這裏將完成初始化操作:

@Override
public void afterPropertiesSet() {
 initHandlerMethods();
}
protected void initHandlerMethods() {
 for (String beanName : getCandidateBeanNames()) {
  if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
   processCandidateBean(beanName);
  }
 }
 handlerMethodsInitialized(getHandlerMethods());
}
protected String[] getCandidateBeanNames() {
 return (this.detectHandlerMethodsInAncestorContexts ?
   BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
   obtainApplicationContext().getBeanNamesForType(Object.class))
;
}
protected void processCandidateBean(String beanName) {
 Class<?> beanType = null;
 try {
  beanType = obtainApplicationContext().getType(beanName);
 }
 catch (Throwable ex) {
 }
 if (beanType != null && isHandler(beanType)) {
  detectHandlerMethods(beanName);
 }
}

可以看到,具體的初始化又是在 initHandlerMethods 方法中完成的,在該方法中,首先調用 getCandidateBeanNames 方法獲取容器中所有的 beanName,然後調用 processCandidateBean 方法對這些候選的 beanName 進行處理,具體的處理思路就是根據 beanName 找到 beanType,然後調用 isHandler 方法判斷該 beanType 是不是一個 Handler,isHandler 是一個空方法,在它的子類 RequestMappingHandlerMapping 中被實現了,該方法主要是檢查該 beanType 上有沒有 @Controller 或者 @RequestMapping 註解,如果有,說明這就是我們想要的 handler,接下來再調用 detectHandlerMethods 方法保存 URL 和 handler 的映射關係:

protected void detectHandlerMethods(Object handler) {
 Class<?> handlerType = (handler instanceof String ?
   obtainApplicationContext().getType((String) handler) : handler.getClass());
 if (handlerType != null) {
  Class<?> userType = ClassUtils.getUserClass(handlerType);
  Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
    (MethodIntrospector.MetadataLookup<T>) method -> {
     try {
      return getMappingForMethod(method, userType);
     }
     catch (Throwable ex) {
      throw new IllegalStateException("Invalid mapping on handler class [" +
        userType.getName() + "]: " + method, ex);
     }
    });
  methods.forEach((method, mapping) -> {
   Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
   registerHandlerMethod(handler, invocableMethod, mapping);
  });
 }
}
  1. 首先找到 handler 的類型 handlerType。
  2. 調用 ClassUtils.getUserClass 方法檢查是否是 cglib 代理的子對象類型,如果是,則返回父類型,否則將參數直接返回。
  3. 接下來調用 MethodIntrospector.selectMethods 方法獲取當前 bean 中所有符合要求的 method。
  4. 遍歷 methods,調用 registerHandlerMethod 方法完成註冊。

上面這段代碼裏又涉及到兩個方法:

  • getMappingForMethod
  • registerHandlerMethod

我們分別來看:

getMappingForMethod

getMappingForMethod 是一個模版方法,具體的實現也是在子類 RequestMappingHandlerMapping 裏邊:

@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
 RequestMappingInfo info = createRequestMappingInfo(method);
 if (info != null) {
  RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
  if (typeInfo != null) {
   info = typeInfo.combine(info);
  }
  String prefix = getPathPrefix(handlerType);
  if (prefix != null) {
   info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
  }
 }
 return info;
}

首先根據 method 對象,調用 createRequestMappingInfo 方法獲取一個 RequestMappingInfo,一個 RequestMappingInfo 包含了一個接口定義的詳細信息,例如參數、header、produces、consumes、請求方法等等信息都在這裏邊。接下來再根據 handlerType 也獲取一個 RequestMappingInfo,並調用 combine 方法將兩個 RequestMappingInfo 進行合併。接下來調用 getPathPrefix 方法查看 handlerType 上有沒有 URL 前綴,如果有,就添加到 info 裏邊去,最後將 info 返回。

這裏要說一下 handlerType 裏邊的這個前綴是那裏來的,我們可以在 Controller 上使用 @RequestMapping 註解,配置一個路徑前綴,這樣 Controller 中的所有方法都加上了該路徑前綴,但是這種方式需要一個一個的配置,如果想一次性配置所有的 Controller 呢?我們可以使用 Spring5.1 中新引入的方法 addPathPrefix 來配置,如下:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setPatternParser(new PathPatternParser()).addPathPrefix("/itboyhub", HandlerTypePredicate.forAnnotation(RestController.class));
    }
}

上面這個配置表示,所有的 @RestController 標記的類都自動加上 itboyhub 前綴。有了這個配置之後,上面的 getPathPrefix 方法獲取到的就是 /itboyhub 了。

registerHandlerMethod

當找齊了 URL 和 handlerMethod 之後,接下來就是將這些信息保存下來,方式如下:

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
 this.mappingRegistry.register(mapping, handler, method);
}
public void register(T mapping, Object handler, Method method) {
 this.readWriteLock.writeLock().lock();
 try {
  HandlerMethod handlerMethod = createHandlerMethod(handler, method);
  validateMethodMapping(handlerMethod, mapping);
  Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);
  for (String path : directPaths) {
   this.pathLookup.add(path, mapping);
  }
  String name = null;
  if (getNamingStrategy() != null) {
   name = getNamingStrategy().getName(handlerMethod, mapping);
   addMappingName(name, handlerMethod);
  }
  CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
  if (corsConfig != null) {
   corsConfig.validateAllowCredentials();
   this.corsLookup.put(handlerMethod, corsConfig);
  }
  this.registry.put(mapping,
    new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));
 }
 finally {
  this.readWriteLock.writeLock().unlock();
 }
}
  1. 首先調用 createHandlerMethod 方法創建 HandlerMethod 對象。
  2. 調用 validateMethodMapping 方法對 handlerMethod 進行驗證,主要是驗證 handlerMethod 是否已經存在。
  3. 從 mappings 中提取出 directPaths,就是不包含通配符的請求路徑,然後將請求路徑和 mapping 的映射關係保存到 pathLookup 中。
  4. 找到所有 handler 的簡稱,調用 addMappingName 方法添加到 nameLookup 中。例如我們在 HelloController 中定義了一個名爲 hello 的請求接口,那麼這裏拿到的就是 HC#hello,HC 是 HelloController 中的大寫字母。
  5. 初始化跨域配置,並添加到 corsLookup 中。
  6. 將構建好的關係添加到 registry 中。

多說一句,第四步這個東西有啥用呢?這個其實是 Spring4 中開始增加的功能,算是一個小彩蛋吧,雖然日常開發很少用,但是我這裏還是和大家說一下。

假如你有如下一個接口:

@RestController
@RequestMapping("/javaboy")
public class HelloController {
    @GetMapping("/aaa")
    public String hello99() {
        return "aaa";
    }
}

當你請求該接口的時候,不想通過路徑,想直接通過方法名,行不行呢?當然可以!

在 jsp 文件中,添加如下超鏈接:

<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<a href="${s:mvcUrl('HC#hello99').build()}">Go!</a>
</body>
</html>

當這個 jsp 頁面渲染完成後,href 屬性就自動成了 hello99 方法的請求路徑了。這個功能的實現,就依賴於前面第四步的內容。

至此,我們就把 AbstractHandlerMethodMapping 的初始化流程看完了。

4.2 請求處理

接下來我們來看下當請求到來後,AbstractHandlerMethodMapping 會如何處理。

和前面第三小節一樣,這裏處理請求的入口方法也是 getHandlerInternal,如下:

@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
 String lookupPath = initLookupPath(request);
 this.mappingRegistry.acquireReadLock();
 try {
  HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
  return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
 }
 finally {
  this.mappingRegistry.releaseReadLock();
 }
}
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 (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);
 }
}

這裏就比較容易,通過 lookupHandlerMethod 找到對應的 HandlerMethod 返回即可,如果 lookupHandlerMethod 方法返回值不爲 null,則通過 createWithResolvedBean 創建 HandlerMethod(主要是確認裏邊的 Bean 等),具體的創建過程松哥在後面的文章中會專門和大家分享。lookupHandlerMethod 方法也比較容易:

  1. 首先根據 lookupPath 找到匹配條件 directPathMatches,然後將獲取到的匹配條件添加到 matches 中(不包含通配符的請求走這裏)。
  2. 如果 matches 爲空,說明根據 lookupPath 沒有找到匹配條件,那麼直接將所有匹配條件加入 matches 中(包含通配符的請求走這裏)。
  3. 對 matches 進行排序,並選擇排序後的第一個爲最佳匹配項,如果前兩個排序相同,則拋出異常。

大致的流程就是這樣,具體到請求並沒有涉及到它的子類。

5.小結

SpringMVC 九大組件,今天和小夥伴們把 HandlerMapping 過了一遍,其實只要認真看,這裏並沒有難點。如果小夥伴們覺得閱讀喫力,也可以在公衆號後臺回覆 ssm,查看松哥錄製的免費入門教程~

剩下的八大組件源碼解析,小夥伴們敬請期待!

本文分享自微信公衆號 - 江南一點雨(a_javaboy)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章