SpringMVC處理器映射器HandlerMapping詳解
前言
在SpringMVC中會有很多請求,每個請求都需要一個HandlerAdapter處理,具體接收到一個請求之后使用哪個HandlerAdapter進行處理呢,他們的過程是什么。本文將對此問題進行討論
DispatcherServlet在初始化中,會調用其initHandlerMappings方法注冊HandlerMapping對象并放到其緩存池中,其過程如下:先查詢容器中是否有處理器映射器,如果有就注冊到其緩存池中,如果沒有就安裝默認到規(guī)則創(chuàng)建處理器映射器,并注冊到其緩存池中。
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
//detectAllHandlerMappings默認為true
//true標志檢測所有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
//如果找不到其他映射,則為默認HandlerMapping。
if (this.handlerMappings == null) {
//從spring-webmvc下的DispatcherServlet.properties讀取默認配置
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}Spring默認HandlerMapping有BeanNameUrlHandlerMapping,RequestMappingHandlerMapping,RouterFunctionMapping

一、處理器映射器架構
處理器映射器使用了策略模式
1、策略接口
HandlerMapping用來查找Handler的。在SpringMVC中會有很多請求,每個請求都需要一個Handler處理,具體接收到一個請求之后使用哪個Handler進行處理呢?這就是HandlerMapping需要做的事
HandlerMapping:負責映射用戶的URL和對應的處理類Handler,HandlerMapping并沒有規(guī)定這個URL與應用的處理類如何映射。所以在HandlerMapping接口中僅僅定義了根據(jù)一個URL必須返回一個由HandlerExecutionChain代表的處理鏈,我們可以在這個處理鏈中添加任意的HandlerAdapter實例來處理這個URL對應的請求(這樣保證了最大的靈活性映射關系)。
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 {
//默認的Handler,這邊使用的Obejct,子類實現(xiàn)的時候,使用HandlerMethod,HandlerExecutionChain等
@Nullable
private Object defaultHandler;
// url路徑計算的輔助類、工具類
private UrlPathHelper urlPathHelper = new UrlPathHelper();
// Ant風格的Path匹配模式~ 解決如/books/{id}場景
private PathMatcher pathMatcher = new AntPathMatcher();
// 保存著攔截器們~~~
private final List<Object> interceptors = new ArrayList<>();
// 從interceptors中解析得到,直接添加給全部handler
private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
// 跨域相關的配置~
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 {
// 給子類擴展:增加攔截器,默認為空實現(xiàn).RequestMappingHandlerMapping也沒有重寫這個方法
extendInterceptors(this.interceptors);
// 找到所有MappedInterceptor(截器是)類型的bean添加到adaptedInterceptors中
detectMappedInterceptors(this.adaptedInterceptors);
// 將interceptors中的攔截器取出放入adaptedInterceptors
// 如果是WebRequestInterceptor類型的攔截器 需要用WebRequestHandlerInterceptorAdapter進行包裝適配
initInterceptors();
}
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//根據(jù)請求獲取對應的處理器,子類實現(xiàn)
Object handler = getHandlerInternal(request);
if (handler == null) {
//如果獲取不到,到默認到處理器中
handler = getDefaultHandler();
}
//如果還沒有處理器,返回null
if (handler == null) {
return null;
}
// 意思是如果當前傳入的handler是個String類型,那就根據(jù)其名字去容器內找這個Bean,當作一個Handler~
if (handler instanceof String) {
String handlerName = (String) handler;
//到容器中找
handler = obtainApplicationContext().getBean(handlerName);
}
//根據(jù)handler和request構造一個請求處理鏈~~
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包括其初始化和調用過程。為了好講解,在這里就將其初始化和調用過程代碼分開說
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,所以這局代碼內容如下
//如果beanType不為null,且類上標注@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(類上標注@Controller注解或者@RequestMapping注解)
2、如果是Handler,獲取這個類上所有標注@RequestMapping的方法信息,以RequestMappingInfo形式
3、把他們儲存到MappingRegistry中
AbstractHandlerMethodMapping.MappingRegistry:內部類注冊中心
維護幾個Map(鍵值對),用來存儲映射的信息, 還有一個MappingRegistration專門保存注冊信息 這個注冊中心,核心是保存了多個Map映射關系,相當于緩存下來。在請求過來時需要查找的時候,可以迅速定位到處理器
class MappingRegistry {
//對于RequestMappingHandlerMapping來說
//保存著RequestMappingInfo和MappingRegistration的對應關系~
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
// 對于保存著mapping和HandlerMethod的對應關系~
//對于RequestMappingHandlerMapping來說
//保存著RequestMappingInfo和HandlerMethod的對應關系~
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的關系~
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
//對于RequestMappingHandlerMapping來說,保存著URL和HandlerMethod的關系~
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映射關系,相當于緩存下來。在請求過來時需要查找的時候,可以迅速定位到處理器
在其初始化過程中,其主要模版化的2個方法
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) {
return null;
}
protected abstract boolean isHandler(Class<?> beanType);三、RequestMappingHandlerMapping映射器模版類的調用
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ù)請求和路徑獲取對應的處理方法,注冊表中取
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,內部就兩個屬性: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()放進matches里面去
addMatchingMappings(directPathMatches, matches, request);
}
// 當還沒有匹配上的時候,別無選擇,只能瀏覽所有映射
// 這里為何要瀏覽所有的mappings呢?而不是報錯404呢?
// 增加路徑匹配對范圍,如:/rest 匹配 /rest.ssss
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
// 只要找到了一個匹配的 就進來這里了~~~
// 請注意:因為到這里 匹配上的可能還不止一個 所以才需要繼續(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調用
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ù)請求獲取對應的handlerMethod過程是:
1、獲取請求路徑
2、根據(jù)路徑到注冊表中查詢對應路徑的RequestMappingInfo
3、如果匹配到多個,就取第一個。
4、如果匹配不到,就到注冊表中查詢所有RequestMappingInfo,匹配規(guī)則我們可以自定義。
Spring MVC請求URL帶后綴匹配的情況,如/hello.json也能匹配/hello
RequestMappingInfoHandlerMapping 在處理http請求的時候, 如果 請求url 有后綴,如果找不到精確匹配的那個@RequestMapping方法。 那么,就把后綴去掉,然后.*去匹配,這樣,一般都可以匹配,默認這個行為是被開啟的。
比如有一個@RequestMapping("/rest"), 那么精確匹配的情況下, 只會匹配/rest請求。 但如果我前端發(fā)來一個 /rest.abcdef 這樣的請求, 又沒有配置 @RequestMapping("/rest.abcdef") 這樣映射的情況下, 那么@RequestMapping("/rest") 就會生效。
這樣會帶來什么問題呢?絕大多數(shù)情況下是沒有問題的,但是如果你是一個對權限要求非常嚴格的系統(tǒng),強烈關閉此項功能,否則你會有意想不到的"收獲"。
究其原因咱們可以接著上面的分析,其實就到了PatternsRequestCondition這個類上,具體實現(xiàn)是它的匹配邏輯來決定的。
public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
...
@Override
@Nullable
public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
// patterns表示此MappingInfo可以匹配的值們。一般對應@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ī)則 非常重要~~~===============
// 注意這個方法的取名,上面是負數(shù),這里是單數(shù)~~~~命名規(guī)范也是有藝術感的
@Nullable
private String getMatchingPattern(String pattern, String lookupPath) {
// 完全相等,那就不繼續(xù)聊了~~~
if (pattern.equals(lookupPath)) {
return pattern;
}
// 注意了:useSuffixPatternMatch 這個屬性就是我們最終要關閉后綴匹配的關鍵
// 這個值默外部給傳的true(其實內部默認值是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也沒有.*這樣匹配的,那就默認你的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ī)則。默認useTrailingSlashMatch它也是true
// 這就是為何我們的/hello/也能匹配上/hello的原因
// 從這可以看出,Spring MVC的寬容度是很高的,容錯處理做得是非常不錯的~~~~~~~
if (this.useTrailingSlashMatch) {
if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
return pattern + "/";
}
}
return null;
}
}分析了URL的匹配原因,現(xiàn)在肯定知道為何默認情況下"/hello.aaaa"或者"/hello.aaaa/“或者”"/hello/""能匹配上我們/hello的原因了吧~~~
Spring和SpringBoot中如何關閉此項功能呢?
為何要關閉的理由,上面其實已經說了。當我們涉及到嚴格的權限校驗(強權限控制)的時候。特備是一些銀行系統(tǒng)、資產系統(tǒng)等等,關閉后綴匹配事非常有必要的。
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware {
private boolean useSuffixPatternMatch = true;
private boolean useTrailingSlashMatch = true;
}可以看到這兩個屬性值都直接冒泡到RequestMappingHandlerMapping這個實現(xiàn)類上來了,所以我們直接通過配置來改變它的默認行為就成。
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
// 關閉后綴名匹配,關閉最后一個/匹配
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
configurer.setUseTrailingSlashMatch(false);
}
}
}**就這么一下,我們的URL就安全了,再也不能后綴名任意匹配了。**在想用后綴匹配,就甩你四個大字:404
到此這篇關于SpringMVC處理器映射器HandlerMapping詳解的文章就介紹到這了,更多相關SpringMVC的HandlerMapping內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳解java如何實現(xiàn)帶RequestBody傳Json參數(shù)的GET請求
在調試Fate平臺時,遇到了一個奇葩的接口類型,該接口為Get方式,入參是一個json類型在body中傳遞,使用body中傳參的話為什么不用POST請求而使用了GET請求,下面我們就來深入研究一下2024-02-02
Java中線程的等待與喚醒_動力節(jié)點Java學院整理
解讀@ConfigurationProperties的基本用法
基于Springboot疫苗接種行程管理系統(tǒng)的設計與實現(xiàn)
Java中String類getBytes()方法詳解與完整實例

