溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

SpringMVC中如何使用HandlerMapping組件

發(fā)布時間:2021-08-12 11:16:28 來源:億速云 閱讀:160 作者:Leah 欄目:開發(fā)技術(shù)

本篇文章為大家展示了SpringMVC中如何使用HandlerMapping組件,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

1.概覽

HandlerMapping 叫做處理器映射器,它的作用就是根據(jù)當前 request 找到對應(yīng)的 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; }

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

HandlerMapping 的繼承關(guān)系如下:

SpringMVC中如何使用HandlerMapping組件

這個繼承關(guān)系雖然看著有點繞,其實仔細觀察就兩大類:

  • AbstractHandlerMethodMapping

  • AbstractUrlHandlerMapping

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

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

2.AbstractHandlerMapping

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

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

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

三個方法都和攔截器有關(guān)。

extendInterceptors

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

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

detectMappedInterceptors

protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {  mappedInterceptors.addAll(BeanFactoryUtils.beansOfTypeIncludingAncestors(    obtainApplicationContext(), MappedInterceptor.class, true, false).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 方法主要是進行攔截器的初始化操作,具體內(nèi)容是將 interceptors 集合中的攔截器添加到  adaptedInterceptors 集合中。

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

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

為什么  AbstractHandlerMapping 中對攔截器如此重視呢?其實不是重視,大家想想,AbstractUrlHandlerMapping 和  AbstractHandlerMethodMapping  最大的區(qū)別在于查找處理器的區(qū)別,一旦處理器找到了,再去找攔截器,但是攔截器都是統(tǒng)一的,并沒有什么明顯區(qū)別,所以攔截器就統(tǒng)一在  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; }

這個方法的執(zhí)行流程是這樣的:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 首先調(diào)用 getHandlerInternal  方法去嘗試獲取處理器,getHandlerInternal 方法也是一個模版方法,該方法將在子類中實現(xiàn)。

  3. 如果沒找到相應(yīng)的處理器,則調(diào)用  getDefaultHandler 方法獲取默認的處理器,我們在配置 HandlerMapping 的時候可以配置默認的處理器。

  4. 如果找到的處理器是一個字符串,則根據(jù)該字符串找去 SpringMVC 容器中找到對應(yīng)的 Bean。

  5. 確保 lookupPath  存在,一會找對應(yīng)的攔截器的時候會用到。

  6. 找到 handler 之后,接下來再調(diào)用 getHandlerExecutionChain 方法獲取  HandlerExecutionChain 對象。

  7. 接下來 if  里邊的是進行跨域處理的,獲取到跨域的相關(guān)配置,然后進行驗證&配置,檢查是否允許跨域??缬蜻@塊的配置以及校驗還是蠻有意思的,松哥以后專門寫文章來和小伙伴們細聊。

接下來我們再來看看第五步的 getHandlerExecutionChain 方法的執(zhí)行邏輯,正是在這個方法里邊把 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; }

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

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

3.AbstractUrlHandlerMapping

AbstractUrlHandlerMapping,看名字就知道,都是按照 URL  地址來進行匹配的,它的原理就是將 URL 地址與對應(yīng)的 Handler 保存在同一個 Map 中,當調(diào)用 getHandlerInternal  方法時,就根據(jù)請求的 URL 去 Map 中找到對應(yīng)的 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. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 首先找到 lookupPath,就是請求的路徑。這個方法本身松哥就不多說了,之前在Spring5 里邊的新玩法!這種 URL  請求讓我漲見識了!一文中有過介紹。

  3. 接下來就是調(diào)用 lookupHandler 方法獲取 Handler 對象,lookupHandler  有一個重載方法,具體用哪個,主要看所使用的 URL 匹配模式,如果使用了最新的 PathPattern(Spring5 之后的),則使用三個參數(shù)的  lookupHandler;如果還是使用之前舊的 AntPathMatcher,則這里使用兩個參數(shù)的 lookupHandler。

  4. 如果前面沒有獲取到 handler 實例,則接下來再做各種嘗試,去分別查找 RootHandler、DefaultHandler 等,如果找到的  Handler 是一個 String,則去 Spring 容器中查找該 String 對應(yīng)的 Bean,再調(diào)用 validateHandler 方法來校驗找到的  handler 和 request 是否匹配,不過這是一個空方法,子類也沒有實現(xiàn),所以可以忽略之。最后再通過 buildPathExposingHandler  方法給找到的 handler 添加一些參數(shù)。

這就是整個 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.這里首先調(diào)用 getDirectMatch 方法直接去 handlerMap 中找對應(yīng)的處理器,handlerMap  中就保存了請求 URL 和處理器的映射關(guān)系,具體的查找過程就是先去 handlerMap 中找,找到了,如果是 String,則去 Spring 容器中找對應(yīng)的  Bean,然后調(diào)用 validateHandler 方法去驗證(實際上沒有驗證,前面已經(jīng)說了),最后調(diào)用 buildPathExposingHandler  方法添加攔截器。

2.如果 getDirectMatch 方法返回值不為 null,則直接將查找到的 handler 返回,方法到此為止。那么什么情況下  getDirectMatch 方法的返回值不為 null  呢?簡單來收就是沒有使用通配符的情況下,請求地址中沒有通配符,一個請求地址對應(yīng)一個處理器,只有這種情況,getDirectMatch 方法返回值才不為  null,因為 handlerMap  中保存的是代碼的定義,比如我們定義代碼的時候,某個處理器的訪問路徑可能帶有通配符,但是當我們真正發(fā)起請求的時候,請求路徑里是沒有通配符的,這個時候再去  handlerMap 中就找不對對應(yīng)的處理器了。如果用到了定義接口時用到了通配符,則需要在下面的代碼中繼續(xù)處理。

3.接下來處理通配符的情況。首先定義  matchingPatterns 集合,將當前請求路徑和 handlerMap 集合中保存的請求路徑規(guī)則進行對比,凡是能匹配上的規(guī)則都直接存入  matchingPatterns 集合中。具體處理中,還有一個 useTrailingSlashMatch 的可能,有的小伙伴 SpringMVC  用的不熟練,看到這里可能就懵了,這里是這樣的,SpringMVC 中,默認是可以匹配結(jié)尾 / 的,舉個簡單例子,如果你定義的接口是/user,那么請求路徑可以是  /user 也可以 /user/,這兩種默認都是支持的,所以這里的 useTrailingSlashMatch 分支主要是處理后面這種情況,處理方式很簡單,就在  registeredPattern 后面加上 / 然后繼續(xù)和請求路徑進行匹配。

4.由于一個請求 URL 可能會和定義的多個接口匹配上,所以  matchingPatterns 變量是一個數(shù)組,接下來就要對 matchingPatterns 進行排序,排序完成后,選擇排序后的第一項作為最佳選項賦值給  bestMatch 變量。默認的排序規(guī)則是 AntPatternComparator,當然開發(fā)者也可以自定義。AntPatternComparator  中定義的優(yōu)先級如下:

路由配置優(yōu)先級
不含任何特殊符號的路徑,如:配置路由/a/b/c第一優(yōu)先級
帶有{}的路徑,如:/a//c第二優(yōu)先級
帶有正則的路徑,如:/a/{regex:\d{3}}/c第三優(yōu)先級
帶有*的路徑,如:/a/b/*第四優(yōu)先級
帶有**的路徑,如:/a/b/**第五優(yōu)先級
最模糊的匹配:/**最低優(yōu)先級

5.找到 bestMatch 之后,接下來再根據(jù) bestMatch 去 handlerMap 中找到對應(yīng)的處理器,直接找如果沒找到,就去檢查  bestMatch 是否以 / 結(jié)尾,如果是以 / 結(jié)尾,則去掉結(jié)尾的 / 再去 handlerMap 中查找,如果還沒找到,那就該拋異常出來了。如果找到的  handler 是 String 類型的,則再去 Spring 容器中查找對應(yīng)的 Bean,接下來再調(diào)用 validateHandler 方法進行驗證。

6.接下來調(diào)用 extractPathWithinPattern 方法提取出映射路徑,例如定義的接口規(guī)則是 myroot/*.html,請求路徑是  myroot/myfile.html,那么最終獲取到的就是myfile.html。

7.接下來的 for  循環(huán)是為了處理存在多個最佳匹配規(guī)則的情況,在第四步中,我們對 matchingPatterns 進行排序,排序完成后,選擇第一項作為最佳選項賦值給  bestMatch,但是最佳選項可能會有多個,這里就是處理最佳選項有多個的情況。

8.最后調(diào)用 buildPathExposingHandler  方法注冊兩個內(nèi)部攔截器,該方法下文我會給大家詳細介紹。

lookupHandler  還有一個重載方法,不過只要大家把這個方法的執(zhí)行流程搞清楚了,重載方法其實很好理解,這里松哥就不再贅述了,唯一要說的就是重載方法用了 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,我們定義的接口和處理器之間的關(guān)系都保存在這個變量中,那么這個變量是怎么初始化的呢?這就涉及到  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)  方法有兩個參數(shù),第一個就是定義的請求路徑,第二個參數(shù)則是處理器 Bean 的名字,第一個參數(shù)是一個數(shù)組,那是因為同一個處理器可以對應(yīng)多個不同的請求路徑。

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

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 如果沒有設(shè)置 lazyInitHandlers,并且 handler 是 String 類型,那么就去 Spring 容器中找到對應(yīng)的 Bean 賦值給  resolvedHandler。

  3. 根據(jù) urlPath 去 handlerMap 中查看是否已經(jīng)有對應(yīng)的處理器了,如果有的話,則拋出異常,一個  URL 地址只能對應(yīng)一個處理器,這個很好理解。

  4. 接下來根據(jù) URL 路徑,將處理器進行配置,最終添加到 handlerMap 變量中。

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

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

3.1  SimpleUrlHandlerMapping

為了方便處理,SimpleUrlHandlerMapping 中自己定義了一個 urlMap  變量,這樣可以在注冊之前做一些預處理,例如確保所有的 URL 都是以 / 開始。SimpleUrlHandlerMapping 在定義時重寫了父類的  initApplicationContext 方法,并在該方法中調(diào)用了 registerHandlers,在 registerHandlers 中又調(diào)用了父類的  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,然后根據(jù) beanName 解析出對應(yīng)的 URL 地址,再將解析出的 url 地址和對應(yīng)的 beanName  注冊到父類的 handlerMap 變量中。換句話說,如果你用了 AbstractDetectingUrlHandlerMapping,就不用像  SimpleUrlHandlerMapping 那樣去挨個配置 URL 地址和處理器的映射關(guān)系了。我們來看下  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 方法,并在該方法中調(diào)用了 detectHandlers 方法,在 detectHandlers  中,首先查找到所有的 beanName,然后調(diào)用 determineUrlsForHandler 方法分析出 beanName 對應(yīng)的 URL,不過這里的  determineUrlsForHandler 方法是一個空方法,具體的實現(xiàn)在它的子類中,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 方法,這個方法的執(zhí)行邏輯也很簡單,就判斷 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,如下圖:

SpringMVC中如何使用HandlerMapping組件

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

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

4.1 初始化流程

AbstractHandlerMethodMapping 類實現(xiàn)了  InitializingBean 接口,所以 Spring 容器會自動調(diào)用其 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  方法中完成的,在該方法中,首先調(diào)用 getCandidateBeanNames 方法獲取容器中所有的 beanName,然后調(diào)用  processCandidateBean 方法對這些候選的 beanName 進行處理,具體的處理思路就是根據(jù) beanName 找到  beanType,然后調(diào)用 isHandler 方法判斷該 beanType 是不是一個 Handler,isHandler 是一個空方法,在它的子類  RequestMappingHandlerMapping 中被實現(xiàn)了,該方法主要是檢查該 beanType 上有沒有 @Controller 或者  @RequestMapping 注解,如果有,說明這就是我們想要的 handler,接下來再調(diào)用 detectHandlerMethods 方法保存 URL 和  handler 的映射關(guān)系:

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. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 首先找到 handler 的類型 handlerType。

  3. 調(diào)用 ClassUtils.getUserClass 方法檢查是否是  cglib 代理的子對象類型,如果是,則返回父類型,否則將參數(shù)直接返回。

  4. 接下來調(diào)用  MethodIntrospector.selectMethods 方法獲取當前 bean 中所有符合要求的 method。

  5. 遍歷  methods,調(diào)用 registerHandlerMethod 方法完成注冊。

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

  • getMappingForMethod

  • registerHandlerMethod

我們分別來看:

getMappingForMethod

getMappingForMethod 是一個模版方法,具體的實現(xiàn)也是在子類  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; }

首先根據(jù) method 對象,調(diào)用 createRequestMappingInfo 方法獲取一個  RequestMappingInfo,一個 RequestMappingInfo  包含了一個接口定義的詳細信息,例如參數(shù)、header、produces、consumes、請求方法等等信息都在這里邊。接下來再根據(jù) handlerType  也獲取一個 RequestMappingInfo,并調(diào)用 combine 方法將兩個 RequestMappingInfo 進行合并。接下來調(diào)用  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. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2. 首先調(diào)用  createHandlerMethod 方法創(chuàng)建 HandlerMethod 對象。

  3. 調(diào)用 validateMethodMapping 方法對  handlerMethod 進行驗證,主要是驗證 handlerMethod 是否已經(jīng)存在。

  4. 從 mappings 中提取出  directPaths,就是不包含通配符的請求路徑,然后將請求路徑和 mapping 的映射關(guān)系保存到 pathLookup 中。

  5. 找到所有  handler 的簡稱,調(diào)用 addMappingName 方法添加到 nameLookup 中。例如我們在 HelloController 中定義了一個名為  hello 的請求接口,那么這里拿到的就是 HC#hello,HC 是 HelloController 中的大寫字母。

  6. 初始化跨域配置,并添加到  corsLookup 中。

  7. 將構(gòu)建好的關(guān)系添加到 registry 中。

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

假如你有如下一個接口:

@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 方法的請求路徑了。這個功能的實現(xiàn),就依賴于前面第四步的內(nèi)容。

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

4.2  請求處理

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

和前面第三小節(jié)一樣,這里處理請求的入口方法也是 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 找到對應(yīng)的 HandlerMethod 返回即可,如果  lookupHandlerMethod 方法返回值不為 null,則通過 createWithResolvedBean 創(chuàng)建  HandlerMethod(主要是確認里邊的 Bean 等),具體的創(chuàng)建過程松哥在后面的文章中會專門和大家分享。lookupHandlerMethod  方法也比較容易:

  1. 首先根據(jù) lookupPath 找到匹配條件 directPathMatches,然后將獲取到的匹配條件添加到 matches  中(不包含通配符的請求走這里)。

  2. 如果 matches 為空,說明根據(jù) lookupPath 沒有找到匹配條件,那么直接將所有匹配條件加入  matches 中(包含通配符的請求走這里)。

  3. 對 matches  進行排序,并選擇排序后的第一個為最佳匹配項,如果前兩個排序相同,則拋出異常。

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

上述內(nèi)容就是SpringMVC中如何使用HandlerMapping組件,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI