您好,登錄后才能下訂單哦!
本篇文章為大家展示了SpringMVC中如何使用HandlerMapping組件,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
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,接下來我們就對這三個關鍵類進行詳細分析。
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.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 方法主要是進行攔截器的初始化操作,具體內容是將 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; }
這個方法的執行流程是這樣的:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
首先調用 getHandlerInternal 方法去嘗試獲取處理器,getHandlerInternal 方法也是一個模版方法,該方法將在子類中實現。
如果沒找到相應的處理器,則調用 getDefaultHandler 方法獲取默認的處理器,我們在配置 HandlerMapping 的時候可以配置默認的處理器。
如果找到的處理器是一個字符串,則根據該字符串找去 SpringMVC 容器中找到對應的 Bean。
確保 lookupPath 存在,一會找對應的攔截器的時候會用到。
找到 handler 之后,接下來再調用 getHandlerExecutionChain 方法獲取 HandlerExecutionChain 對象。
接下來 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 在子類中實現,接下來我們就來看看它的子類。
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; }
鴻蒙官方戰略合作共建——HarmonyOS技術社區
首先找到 lookupPath,就是請求的路徑。這個方法本身松哥就不多說了,之前在Spring5 里邊的新玩法!這種 URL 請求讓我漲見識了!一文中有過介紹。
接下來就是調用 lookupHandler 方法獲取 Handler 對象,lookupHandler 有一個重載方法,具體用哪個,主要看所使用的 URL 匹配模式,如果使用了最新的 PathPattern(Spring5 之后的),則使用三個參數的 lookupHandler;如果還是使用之前舊的 AntPathMatcher,則這里使用兩個參數的 lookupHandler。
如果前面沒有獲取到 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/** | 第五優先級 |
最模糊的匹配:/** | 最低優先級 |
5.找到 bestMatch 之后,接下來再根據 bestMatch 去 handlerMap 中找到對應的處理器,直接找如果沒找到,就去檢查 bestMatch 是否以 / 結尾,如果是以 / 結尾,則去掉結尾的 / 再去 handlerMap 中查找,如果還沒找到,那就該拋異常出來了。如果找到的 handler 是 String 類型的,則再去 Spring 容器中查找對應的 Bean,接下來再調用 validateHandler 方法進行驗證。
6.接下來調用 extractPathWithinPattern 方法提取出映射路徑,例如定義的接口規則是 myroot/*.html,請求路徑是 myroot/myfile.html,那么最終獲取到的就是myfile.html。
7.接下來的 for 循環是為了處理存在多個最佳匹配規則的情況,在第四步中,我們對 matchingPatterns 進行排序,排序完成后,選擇第一項作為最佳選項賦值給 bestMatch,但是最佳選項可能會有多個,這里就是處理最佳選項有多個的情況。
8.最后調用 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 的初始化,具體流程如下:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
如果沒有設置 lazyInitHandlers,并且 handler 是 String 類型,那么就去 Spring 容器中找到對應的 Bean 賦值給 resolvedHandler。
根據 urlPath 去 handlerMap 中查看是否已經有對應的處理器了,如果有的話,則拋出異常,一個 URL 地址只能對應一個處理器,這個很好理解。
接下來根據 URL 路徑,將處理器進行配置,最終添加到 handlerMap 變量中。
這就是 AbstractUrlHandlerMapping 的主要工作,其中 registerHandler 將在它的子類中調用。
接下來我們來看 AbstractUrlHandlerMapping 的子類。
為了方便處理,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>
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 體系下的東西就和大家分享完了。
AbstractHandlerMethodMapping 體系下只有三個類,分別是 AbstractHandlerMethodMapping、RequestMappingInfoHandlerMapping 以及 RequestMappingHandlerMapping,如下圖:
在前面第三小節的 AbstractUrlHandlerMapping 體系下,一個 Handler 一般就是一個類,但是在 AbstractHandlerMethodMapping 體系下,一個 Handler 就是一個 Mehtod,這也是我們目前使用 SpringMVC 時最常見的用法,即直接用 @RequestMapping 去標記一個方法,該方法就是一個 Handler。
接下來我們就一起來看看 AbstractHandlerMethodMapping。
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); }); } }
鴻蒙官方戰略合作共建——HarmonyOS技術社區
首先找到 handler 的類型 handlerType。
調用 ClassUtils.getUserClass 方法檢查是否是 cglib 代理的子對象類型,如果是,則返回父類型,否則將參數直接返回。
接下來調用 MethodIntrospector.selectMethods 方法獲取當前 bean 中所有符合要求的 method。
遍歷 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(); } }
鴻蒙官方戰略合作共建——HarmonyOS技術社區
首先調用 createHandlerMethod 方法創建 HandlerMethod 對象。
調用 validateMethodMapping 方法對 handlerMethod 進行驗證,主要是驗證 handlerMethod 是否已經存在。
從 mappings 中提取出 directPaths,就是不包含通配符的請求路徑,然后將請求路徑和 mapping 的映射關系保存到 pathLookup 中。
找到所有 handler 的簡稱,調用 addMappingName 方法添加到 nameLookup 中。例如我們在 HelloController 中定義了一個名為 hello 的請求接口,那么這里拿到的就是 HC#hello,HC 是 HelloController 中的大寫字母。
初始化跨域配置,并添加到 corsLookup 中。
將構建好的關系添加到 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 的初始化流程看完了。
接下來我們來看下當請求到來后,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 方法也比較容易:
首先根據 lookupPath 找到匹配條件 directPathMatches,然后將獲取到的匹配條件添加到 matches 中(不包含通配符的請求走這里)。
如果 matches 為空,說明根據 lookupPath 沒有找到匹配條件,那么直接將所有匹配條件加入 matches 中(包含通配符的請求走這里)。
對 matches 進行排序,并選擇排序后的第一個為最佳匹配項,如果前兩個排序相同,則拋出異常。
大致的流程就是這樣,具體到請求并沒有涉及到它的子類。
上述內容就是SpringMVC中如何使用HandlerMapping組件,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。