SpringMVC處理器映射器HandlerMapping詳解
前言
在SpringMVC中會有很多請求,每個請求都需要一個HandlerAdapter處理,具體接收到一個請求之后使用哪個HandlerAdapter進(jìn)行處理呢,他們的過程是什么。本文將對此問題進(jìn)行討論
DispatcherServlet在初始化中,會調(diào)用其initHandlerMappings方法注冊HandlerMapping對象并放到其緩存池中,其過程如下:先查詢?nèi)萜髦惺欠裼刑幚砥饔成淦?,如果有就注冊到其緩存池中,如果沒有就安裝默認(rèn)到規(guī)則創(chuàng)建處理器映射器,并注冊到其緩存池中。
private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; //detectAllHandlerMappings默認(rèn)為true //true標(biāo)志檢測所有handlerMapping,false只獲取“handlerMapping”bean。 if (this.detectAllHandlerMappings) { // 在ApplicationContext中查找所有HandlerMappings,包括祖先上下文。 Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<>(matchingBeans.values()); //排序 AnnotationAwareOrderComparator.sort(this.handlerMappings); } } else { try { //只獲取“handlerMapping”bean HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. } } //通過注冊,確保我們至少有一個HandlerMapping //如果找不到其他映射,則為默認(rèn)HandlerMapping。 if (this.handlerMappings == null) { //從spring-webmvc下的DispatcherServlet.properties讀取默認(rèn)配置 this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); if (logger.isTraceEnabled()) { logger.trace("No HandlerMappings declared for servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties"); } } }
Spring默認(rèn)HandlerMapping有BeanNameUrlHandlerMapping,RequestMappingHandlerMapping,RouterFunctionMapping
一、處理器映射器架構(gòu)
處理器映射器使用了策略模式
1、策略接口
HandlerMapping用來查找Handler的。在SpringMVC中會有很多請求,每個請求都需要一個Handler處理,具體接收到一個請求之后使用哪個Handler進(jìn)行處理呢?這就是HandlerMapping需要做的事
HandlerMapping:負(fù)責(zé)映射用戶的URL和對應(yīng)的處理類Handler,HandlerMapping并沒有規(guī)定這個URL與應(yīng)用的處理類如何映射。所以在HandlerMapping接口中僅僅定義了根據(jù)一個URL必須返回一個由HandlerExecutionChain代表的處理鏈,我們可以在這個處理鏈中添加任意的HandlerAdapter實例來處理這個URL對應(yīng)的請求(這樣保證了最大的靈活性映射關(guān)系)。
public interface HandlerMapping { ...//忽略一些常量 @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; }
2、請求鏈
public class HandlerExecutionChain { //處理器 private final Object handler; //攔截器 private HandlerInterceptor[] interceptors; //攔截器 private List<HandlerInterceptor> interceptorList; //忽略代碼.... }
3、模版類
處理器映射器都是實現(xiàn)AbstractHandlerMapping,該抽象類完成了所有的Handler以及handler里面所有的HandlerMethod的模版操作,但是怎么獲取Handler,這些邏輯都是交給子類自己去實現(xiàn),所以這層抽象可謂也是非常的靈活,并沒有把Handler的實現(xiàn)方式定死。
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered, BeanNameAware { //默認(rèn)的Handler,這邊使用的Obejct,子類實現(xiàn)的時候,使用HandlerMethod,HandlerExecutionChain等 @Nullable private Object defaultHandler; // url路徑計算的輔助類、工具類 private UrlPathHelper urlPathHelper = new UrlPathHelper(); // Ant風(fēng)格的Path匹配模式~ 解決如/books/{id}場景 private PathMatcher pathMatcher = new AntPathMatcher(); // 保存著攔截器們~~~ private final List<Object> interceptors = new ArrayList<>(); // 從interceptors中解析得到,直接添加給全部handler private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>(); // 跨域相關(guān)的配置~ private CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource(); private CorsProcessor corsProcessor = new DefaultCorsProcessor(); // 最低的順序(default: same as non-Ordered) private int order = Ordered.LOWEST_PRECEDENCE; @Nullable private String beanName; /** * Initializes the interceptors. * @see #extendInterceptors(java.util.List) * @see #initInterceptors() */ @Override protected void initApplicationContext() throws BeansException { // 給子類擴(kuò)展:增加攔截器,默認(rèn)為空實現(xiàn).RequestMappingHandlerMapping也沒有重寫這個方法 extendInterceptors(this.interceptors); // 找到所有MappedInterceptor(截器是)類型的bean添加到adaptedInterceptors中 detectMappedInterceptors(this.adaptedInterceptors); // 將interceptors中的攔截器取出放入adaptedInterceptors // 如果是WebRequestInterceptor類型的攔截器 需要用WebRequestHandlerInterceptorAdapter進(jìn)行包裝適配 initInterceptors(); } @Override @Nullable public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { //根據(jù)請求獲取對應(yīng)的處理器,子類實現(xiàn) Object handler = getHandlerInternal(request); if (handler == null) { //如果獲取不到,到默認(rèn)到處理器中 handler = getDefaultHandler(); } //如果還沒有處理器,返回null if (handler == null) { return null; } // 意思是如果當(dāng)前傳入的handler是個String類型,那就根據(jù)其名字去容器內(nèi)找這個Bean,當(dāng)作一個Handler~ if (handler instanceof String) { String handlerName = (String) handler; //到容器中找 handler = obtainApplicationContext().getBean(handlerName); } //根據(jù)handler和request構(gòu)造一個請求處理鏈~~ HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); // 4.2版本提供了對CORS跨域資源共享的支持 此處暫時略過~ if (hasCorsConfigurationSource(handler)) { CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); config = (config != null ? config.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; } @Nullable protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception; }
接下來最重要的就是以getHandlerInternal()方法為主線,看看其子類們的實現(xiàn)。它主要分為兩大主線: AbstractUrlHandlerMapping
和 AbstractHandlerMethodMapping
。
本文是以AbstractHandlerMethodMapping的子類RequestMappingHandlerMapping為主線
二、RequestMappingHandlerMapping的初始化
HandlerMethod映射器都是是處理器映射器的一種類型的映射器。這種類型的映射器有一個模版類AbstractHandlerMethodMapping 所有的HandlerMethod映射器都是實現(xiàn)他的
AbstractHandlerMethodMapping包括其初始化和調(diào)用過程。為了好講解,在這里就將其初始化和調(diào)用過程代碼分開說
HandlerMethod映射器模版類的初始化
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean { private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle")); private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration(); static { ALLOW_CORS_CONFIG.addAllowedOrigin("*"); ALLOW_CORS_CONFIG.addAllowedMethod("*"); ALLOW_CORS_CONFIG.addAllowedHeader("*"); ALLOW_CORS_CONFIG.setAllowCredentials(true); } private boolean detectHandlerMethodsInAncestorContexts = false; @Nullable private HandlerMethodMappingNamingStrategy<T> namingStrategy; //注冊表,HandlerMapping在容器啟動過程中初始化,把掃描到的handler放到注冊表中 private final MappingRegistry mappingRegistry = new MappingRegistry(); @Override public void afterPropertiesSet() { initHandlerMethods(); } protected void initHandlerMethods() { //循環(huán)所有的bean for (String beanName : getCandidateBeanNames()) { //如果bean名字不是以scopedTarget.開頭 if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { processCandidateBean(beanName); } } //日志輸出 handlerMethodsInitialized(getHandlerMethods()); } protected void processCandidateBean(String beanName) { Class<?> beanType = null; try { beanType = obtainApplicationContext().getType(beanName); } catch (Throwable ex) { if (logger.isTraceEnabled()) { logger.trace("Could not resolve type for bean '" + beanName + "'", ex); } } //因為這里我們是研究RequestMappingHandlerMapping,所以這局代碼內(nèi)容如下 //如果beanType不為null,且類上標(biāo)注@Controller注解或者@RequestMapping注解 if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } } 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); } }); if (logger.isTraceEnabled()) { logger.trace(formatMappings(userType, methods)); } methods.forEach((method, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); //底層使用了MappingRegistry的register方法 registerHandlerMethod(handler, invocableMethod, mapping); }); } } //忽略處理器映射器查詢Handler部分代碼..... }
1、循環(huán)所有的bean,如果bean名字不是以scopedTarget.開頭,那么就判斷他們是否是Handler(類上標(biāo)注@Controller注解或者@RequestMapping注解)
2、如果是Handler,獲取這個類上所有標(biāo)注@RequestMapping的方法信息,以RequestMappingInfo形式
3、把他們儲存到MappingRegistry中
AbstractHandlerMethodMapping.MappingRegistry:內(nèi)部類注冊中心
維護(hù)幾個Map(鍵值對),用來存儲映射的信息, 還有一個MappingRegistration專門保存注冊信息 這個注冊中心,核心是保存了多個Map映射關(guān)系,相當(dāng)于緩存下來。在請求過來時需要查找的時候,可以迅速定位到處理器
class MappingRegistry { //對于RequestMappingHandlerMapping來說 //保存著RequestMappingInfo和MappingRegistration的對應(yīng)關(guān)系~ private final Map<T, MappingRegistration<T>> registry = new HashMap<>(); // 對于保存著mapping和HandlerMethod的對應(yīng)關(guān)系~ //對于RequestMappingHandlerMapping來說 //保存著RequestMappingInfo和HandlerMethod的對應(yīng)關(guān)系~ private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>(); // 這里的Map不是普通的Map,而是MultiValueMap,它是個多值Map。其實它的value是一個list類型的值 // 至于為何是多值?有這么一種情況 URL都是/api/v1/hello 但是有的是get post delete等方法 所以有可能是會匹配到多個MappingInfo的 //對于RequestMappingHandlerMapping來說,保存著URL和RequestMappingInfo的關(guān)系~ private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>(); //對于RequestMappingHandlerMapping來說,保存著URL和HandlerMethod的關(guān)系~ private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>() // 這兩個就不用解釋了 private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>(); // 讀寫鎖~~~ 讀寫分離 提高啟動效率 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //5.1版本其子類只有一個RequestMappingHandlerMapping,T就是RequestMappingInfo //handler一般情況下是處理器方法從屬bean的名字 //method是處理器方法 public void register(T mapping, Object handler, Method method) { if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) { throw new IllegalStateException("Unsupported suspending handler method detected: " + method); } this.readWriteLock.writeLock().lock(); try { HandlerMethod handlerMethod = createHandlerMethod(handler, method); //斷言提供的映射是唯一的。 validateMethodMapping(handlerMethod, mapping); this.mappingLookup.put(mapping, handlerMethod); List<String> directUrls = getDirectUrls(mapping); for (String url : directUrls) { this.urlLookup.add(url, mapping); } String name = null; if (getNamingStrategy() != null) { name = getNamingStrategy().getName(handlerMethod, mapping); addMappingName(name, handlerMethod); } //初始化跨域配置 //使用的是AbstractHandlerMethodMapping的initCorsConfiguration方法,子類實現(xiàn) CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { this.corsLookup.put(handlerMethod, corsConfig); } this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name)); } finally { this.readWriteLock.writeLock().unlock(); } }
這個注冊中心,核心是保存了多個Map映射關(guān)系,相當(dāng)于緩存下來。在請求過來時需要查找的時候,可以迅速定位到處理器
在其初始化過程中,其主要模版化的2個方法
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) { return null; } protected abstract boolean isHandler(Class<?> beanType);
三、RequestMappingHandlerMapping映射器模版類的調(diào)用
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean { private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle")); private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration(); static { ALLOW_CORS_CONFIG.addAllowedOrigin("*"); ALLOW_CORS_CONFIG.addAllowedMethod("*"); ALLOW_CORS_CONFIG.addAllowedHeader("*"); ALLOW_CORS_CONFIG.setAllowCredentials(true); } private boolean detectHandlerMethodsInAncestorContexts = false; @Nullable private HandlerMethodMappingNamingStrategy<T> namingStrategy; //注冊表,HandlerMapping在容器啟動過程中初始化,把掃描到的handler放到注冊表中 private final MappingRegistry mappingRegistry = new MappingRegistry(); //忽略初始化部分代碼..... protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { //獲取請求路徑 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); //放到請求屬性中 request.setAttribute(LOOKUP_PATH, lookupPath); this.mappingRegistry.acquireReadLock(); try { //根據(jù)請求和路徑獲取對應(yīng)的處理方法,注冊表中取 HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } finally { this.mappingRegistry.releaseReadLock(); } } protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { // Match是一個private class,內(nèi)部就兩個屬性:T mapping和HandlerMethod handlerMethod List<Match> matches = new ArrayList<>(); // 根據(jù)lookupPath去注冊中心里查找RequestMappingInfo,因為一個具體的url可能匹配上多個RequestMappingInfo // 至于為何是多值?有這么一種情況 URL都是/api/v1/hello 但是有的是get post delete等方法 等不一樣,都算多個的 所以有可能是會匹配到多個MappingInfo的 // 所有這個里可以匹配出多個出來。比如/hello 匹配出GET、POST、PUT都成,所以size可以為3 List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { // 依賴于子類實現(xiàn)的抽象方法:getMatchingMapping() 看看到底匹不匹配,而不僅僅是URL匹配就行 // 比如還有method、headers、consumes等等這些不同都代表著不同的MappingInfo的 // 最終匹配上的,會new Match()放進(jìn)matches里面去 addMatchingMappings(directPathMatches, matches, request); } // 當(dāng)還沒有匹配上的時候,別無選擇,只能瀏覽所有映射 // 這里為何要瀏覽所有的mappings呢?而不是報錯404呢? // 增加路徑匹配對范圍,如:/rest 匹配 /rest.ssss if (matches.isEmpty()) { addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } // 只要找到了一個匹配的 就進(jìn)來這里了~~~ // 請注意:因為到這里 匹配上的可能還不止一個 所以才需要繼續(xù)處理~~ if (!matches.isEmpty()) { Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); matches.sort(comparator); //如果匹配到多個,就取第一個 Match bestMatch = matches.get(0); if (matches.size() > 1) { if (logger.isTraceEnabled()) { logger.trace(matches.size() + " matching mappings: " + matches); } if (CorsUtils.isPreFlightRequest(request)) { return PREFLIGHT_AMBIGUOUS_MATCH; } Match secondBestMatch = matches.get(1); if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.handlerMethod.getMethod(); Method m2 = secondBestMatch.handlerMethod.getMethod(); String uri = request.getRequestURI(); throw new IllegalStateException(" "); } } request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod); //請求域增加一些屬性,子類重寫 handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.handlerMethod; } else { //請求域增加一些屬性,子類重寫 return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); } } } protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) { request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath); } @Nullable protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request) throws Exception { return null; }
RequestMappingHandlerMapping調(diào)用
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> { private static final Method HTTP_OPTIONS_HANDLE_METHOD; /** * Expose URI template variables, matrix variables, and producible media types in the request. * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE */ @Override protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) { super.handleMatch(info, lookupPath, request); String bestPattern; Map<String, String> uriVariables; Set<String> patterns = info.getPatternsCondition().getPatterns(); if (patterns.isEmpty()) { bestPattern = lookupPath; uriVariables = Collections.emptyMap(); } else { bestPattern = patterns.iterator().next(); uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath); } request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern); if (isMatrixVariableContentAvailable()) { Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables); request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars); } Map<String, String> decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables); if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) { Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes(); request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); } } private boolean isMatrixVariableContentAvailable() { return !getUrlPathHelper().shouldRemoveSemicolonContent(); } private Map<String, MultiValueMap<String, String>> extractMatrixVariables( HttpServletRequest request, Map<String, String> uriVariables) { Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<>(); uriVariables.forEach((uriVarKey, uriVarValue) -> { int equalsIndex = uriVarValue.indexOf('='); if (equalsIndex == -1) { return; } int semicolonIndex = uriVarValue.indexOf(';'); if (semicolonIndex != -1 && semicolonIndex != 0) { uriVariables.put(uriVarKey, uriVarValue.substring(0, semicolonIndex)); } String matrixVariables; if (semicolonIndex == -1 || semicolonIndex == 0 || equalsIndex < semicolonIndex) { matrixVariables = uriVarValue; } else { matrixVariables = uriVarValue.substring(semicolonIndex + 1); } MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables); result.put(uriVarKey, getUrlPathHelper().decodeMatrixVariables(request, vars)); }); return result; } /** * Iterate all RequestMappingInfo's once again, look if any match by URL at * least and raise exceptions according to what doesn't match. * @throws HttpRequestMethodNotSupportedException if there are matches by URL * but not by HTTP method * @throws HttpMediaTypeNotAcceptableException if there are matches by URL * but not by consumable/producible media types */ @Override protected HandlerMethod handleNoMatch( Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException { PartialMatchHelper helper = new PartialMatchHelper(infos, request); if (helper.isEmpty()) { return null; } if (helper.hasMethodsMismatch()) { Set<String> methods = helper.getAllowedMethods(); if (HttpMethod.OPTIONS.matches(request.getMethod())) { HttpOptionsHandler handler = new HttpOptionsHandler(methods); return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD); } throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods); } if (helper.hasConsumesMismatch()) { Set<MediaType> mediaTypes = helper.getConsumableMediaTypes(); MediaType contentType = null; if (StringUtils.hasLength(request.getContentType())) { try { contentType = MediaType.parseMediaType(request.getContentType()); } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotSupportedException(ex.getMessage()); } } throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes)); } if (helper.hasProducesMismatch()) { Set<MediaType> mediaTypes = helper.getProducibleMediaTypes(); throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes)); } if (helper.hasParamsMismatch()) { List<String[]> conditions = helper.getParamConditions(); throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap()); } return null; } }
RequestMappingHandlerMapping根據(jù)請求獲取對應(yīng)的handlerMethod過程是:
1、獲取請求路徑
2、根據(jù)路徑到注冊表中查詢對應(yīng)路徑的RequestMappingInfo
3、如果匹配到多個,就取第一個。
4、如果匹配不到,就到注冊表中查詢所有RequestMappingInfo,匹配規(guī)則我們可以自定義。
Spring MVC請求URL帶后綴匹配的情況,如/hello.json也能匹配/hello
RequestMappingInfoHandlerMapping 在處理http請求的時候, 如果 請求url 有后綴,如果找不到精確匹配的那個@RequestMapping方法。 那么,就把后綴去掉,然后.*去匹配,這樣,一般都可以匹配,默認(rèn)這個行為是被開啟的。
比如有一個@RequestMapping("/rest"), 那么精確匹配的情況下, 只會匹配/rest請求。 但如果我前端發(fā)來一個 /rest.abcdef 這樣的請求, 又沒有配置 @RequestMapping("/rest.abcdef") 這樣映射的情況下, 那么@RequestMapping("/rest") 就會生效。
這樣會帶來什么問題呢?絕大多數(shù)情況下是沒有問題的,但是如果你是一個對權(quán)限要求非常嚴(yán)格的系統(tǒng),強(qiáng)烈關(guān)閉此項功能,否則你會有意想不到的"收獲"。
究其原因咱們可以接著上面的分析,其實就到了PatternsRequestCondition這個類上,具體實現(xiàn)是它的匹配邏輯來決定的。
public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> { ... @Override @Nullable public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) { // patterns表示此MappingInfo可以匹配的值們。一般對應(yīng)@RequestMapping注解上的patters數(shù)組的值 if (this.patterns.isEmpty()) { return this; } // 拿到待匹配的值,比如此處為"/hello.json" String lookupPath = this.pathHelper.getLookupPathForRequest(request); // 最主要就是這個方法了,它拿著這個lookupPath匹配~~~~ List<String> matches = getMatchingPatterns(lookupPath); // 此處如果為empty,就返回null了~~~~ return (!matches.isEmpty() ? new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions) : null); } public List<String> getMatchingPatterns(String lookupPath) { List<String> matches = new ArrayList<>(); for (String pattern : this.patterns) { // 最最最重點就是在getMatchingPattern()這個方法里~~~ 拿著lookupPath和pattern看它倆合拍不~ String match = getMatchingPattern(pattern, lookupPath); if (match != null) { matches.add(match); } } // 解釋一下為何匹配的可能是多個。因為url匹配上了,但是還有可能@RequestMapping的其余屬性匹配不上啊,所以此處需要注意 是可能匹配上多個的 最終是唯一匹配就成~ if (matches.size() > 1) { matches.sort(this.pathMatcher.getPatternComparator(lookupPath)); } return matches; } // // ===============url的真正匹配規(guī)則 非常重要~~~=============== // 注意這個方法的取名,上面是負(fù)數(shù),這里是單數(shù)~~~~命名規(guī)范也是有藝術(shù)感的 @Nullable private String getMatchingPattern(String pattern, String lookupPath) { // 完全相等,那就不繼續(xù)聊了~~~ if (pattern.equals(lookupPath)) { return pattern; } // 注意了:useSuffixPatternMatch 這個屬性就是我們最終要關(guān)閉后綴匹配的關(guān)鍵 // 這個值默外部給傳的true(其實內(nèi)部默認(rèn)值是boolean類型為false) if (this.useSuffixPatternMatch) { // 這個意思是若useSuffixPatternMatch=true我們支持后綴匹配。我們還可以配置fileExtensions讓只支持我們自定義的指定的后綴匹配,而不是下面最終的.*全部支持 if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) { for (String extension : this.fileExtensions) { if (this.pathMatcher.match(pattern + extension, lookupPath)) { return pattern + extension; } } } // 若你沒有配置指定后綴匹配,并且你的handler也沒有.*這樣匹配的,那就默認(rèn)你的pattern就給你添加上后綴".*",表示匹配所有請求的url的后綴~~~ else { boolean hasSuffix = pattern.indexOf('.') != -1; if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) { return pattern + ".*"; } } } // 若匹配上了 直接返回此patter if (this.pathMatcher.match(pattern, lookupPath)) { return pattern; } // 這又是它支持的匹配規(guī)則。默認(rèn)useTrailingSlashMatch它也是true // 這就是為何我們的/hello/也能匹配上/hello的原因 // 從這可以看出,Spring MVC的寬容度是很高的,容錯處理做得是非常不錯的~~~~~~~ if (this.useTrailingSlashMatch) { if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) { return pattern + "/"; } } return null; } }
分析了URL的匹配原因,現(xiàn)在肯定知道為何默認(rèn)情況下"/hello.aaaa"或者"/hello.aaaa/“或者”"/hello/""能匹配上我們/hello的原因了吧~~~
Spring和SpringBoot中如何關(guān)閉此項功能呢?
為何要關(guān)閉的理由,上面其實已經(jīng)說了。當(dāng)我們涉及到嚴(yán)格的權(quán)限校驗(強(qiáng)權(quán)限控制)的時候。特備是一些銀行系統(tǒng)、資產(chǎn)系統(tǒng)等等,關(guān)閉后綴匹配事非常有必要的。
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware { private boolean useSuffixPatternMatch = true; private boolean useTrailingSlashMatch = true; }
可以看到這兩個屬性值都直接冒泡到RequestMappingHandlerMapping這個實現(xiàn)類上來了,所以我們直接通過配置來改變它的默認(rèn)行為就成。
@Configuration @EnableWebMvc public class WebMvcConfig implements WebMvcConfigurer { // 關(guān)閉后綴名匹配,關(guān)閉最后一個/匹配 @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setUseSuffixPatternMatch(false); configurer.setUseTrailingSlashMatch(false); } } }
**就這么一下,我們的URL就安全了,再也不能后綴名任意匹配了。**在想用后綴匹配,就甩你四個大字:404
到此這篇關(guān)于SpringMVC處理器映射器HandlerMapping詳解的文章就介紹到這了,更多相關(guān)SpringMVC的HandlerMapping內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解java如何實現(xiàn)帶RequestBody傳Json參數(shù)的GET請求
在調(diào)試Fate平臺時,遇到了一個奇葩的接口類型,該接口為Get方式,入?yún)⑹且粋€json類型在body中傳遞,使用body中傳參的話為什么不用POST請求而使用了GET請求,下面我們就來深入研究一下2024-02-02

Java中線程的等待與喚醒_動力節(jié)點Java學(xué)院整理

詳解RocketMQ中的消費(fèi)者啟動與消費(fèi)流程分析

解讀@ConfigurationProperties的基本用法

idea中項目前端網(wǎng)頁圖標(biāo)不顯示的原因及解決

基于Springboot疫苗接種行程管理系統(tǒng)的設(shè)計與實現(xiàn)

Java中String類getBytes()方法詳解與完整實例