SpringMVC處理器映射器HandlerMapping詳解
前言
在SpringMVC中會(huì)有很多請(qǐng)求,每個(gè)請(qǐng)求都需要一個(gè)HandlerAdapter處理,具體接收到一個(gè)請(qǐng)求之后使用哪個(gè)HandlerAdapter進(jìn)行處理呢,他們的過程是什么。本文將對(duì)此問題進(jìn)行討論
DispatcherServlet在初始化中,會(huì)調(diào)用其initHandlerMappings方法注冊HandlerMapping對(duì)象并放到其緩存池中,其過程如下:先查詢?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.
}
}
//通過注冊,確保我們至少有一個(gè)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中會(huì)有很多請(qǐng)求,每個(gè)請(qǐng)求都需要一個(gè)Handler處理,具體接收到一個(gè)請(qǐng)求之后使用哪個(gè)Handler進(jìn)行處理呢?這就是HandlerMapping需要做的事
HandlerMapping:負(fù)責(zé)映射用戶的URL和對(duì)應(yīng)的處理類Handler,HandlerMapping并沒有規(guī)定這個(gè)URL與應(yīng)用的處理類如何映射。所以在HandlerMapping接口中僅僅定義了根據(jù)一個(gè)URL必須返回一個(gè)由HandlerExecutionChain代表的處理鏈,我們可以在這個(gè)處理鏈中添加任意的HandlerAdapter實(shí)例來處理這個(gè)URL對(duì)應(yīng)的請(qǐng)求(這樣保證了最大的靈活性映射關(guān)系)。
public interface HandlerMapping {
...//忽略一些常量
@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}2、請(qǐng)求鏈
public class HandlerExecutionChain {
//處理器
private final Object handler;
//攔截器
private HandlerInterceptor[] interceptors;
//攔截器
private List<HandlerInterceptor> interceptorList;
//忽略代碼....
}3、模版類
處理器映射器都是實(shí)現(xiàn)AbstractHandlerMapping,該抽象類完成了所有的Handler以及handler里面所有的HandlerMethod的模版操作,但是怎么獲取Handler,這些邏輯都是交給子類自己去實(shí)現(xiàn),所以這層抽象可謂也是非常的靈活,并沒有把Handler的實(shí)現(xiàn)方式定死。
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
//默認(rèn)的Handler,這邊使用的Obejct,子類實(shí)現(xiàn)的時(shí)候,使用HandlerMethod,HandlerExecutionChain等
@Nullable
private Object defaultHandler;
// url路徑計(jì)算的輔助類、工具類
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)為空實(shí)現(xiàn).RequestMappingHandlerMapping也沒有重寫這個(gè)方法
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ù)請(qǐng)求獲取對(duì)應(yīng)的處理器,子類實(shí)現(xiàn)
Object handler = getHandlerInternal(request);
if (handler == null) {
//如果獲取不到,到默認(rèn)到處理器中
handler = getDefaultHandler();
}
//如果還沒有處理器,返回null
if (handler == null) {
return null;
}
// 意思是如果當(dāng)前傳入的handler是個(gè)String類型,那就根據(jù)其名字去容器內(nèi)找這個(gè)Bean,當(dāng)作一個(gè)Handler~
if (handler instanceof String) {
String handlerName = (String) handler;
//到容器中找
handler = obtainApplicationContext().getBean(handlerName);
}
//根據(jù)handler和request構(gòu)造一個(gè)請(qǐng)求處理鏈~~
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
// 4.2版本提供了對(duì)CORS跨域資源共享的支持 此處暫時(shí)略過~
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()方法為主線,看看其子類們的實(shí)現(xiàn)。它主要分為兩大主線: AbstractUrlHandlerMapping 和 AbstractHandlerMethodMapping 。
本文是以AbstractHandlerMethodMapping的子類RequestMappingHandlerMapping為主線

二、RequestMappingHandlerMapping的初始化
HandlerMethod映射器都是是處理器映射器的一種類型的映射器。這種類型的映射器有一個(gè)模版類AbstractHandlerMethodMapping 所有的HandlerMethod映射器都是實(shí)現(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在容器啟動(dòng)過程中初始化,把掃描到的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);
}
}
//因?yàn)檫@里我們是研究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,獲取這個(gè)類上所有標(biāo)注@RequestMapping的方法信息,以RequestMappingInfo形式
3、把他們儲(chǔ)存到MappingRegistry中
AbstractHandlerMethodMapping.MappingRegistry:內(nèi)部類注冊中心
維護(hù)幾個(gè)Map(鍵值對(duì)),用來存儲(chǔ)映射的信息, 還有一個(gè)MappingRegistration專門保存注冊信息 這個(gè)注冊中心,核心是保存了多個(gè)Map映射關(guān)系,相當(dāng)于緩存下來。在請(qǐng)求過來時(shí)需要查找的時(shí)候,可以迅速定位到處理器
class MappingRegistry {
//對(duì)于RequestMappingHandlerMapping來說
//保存著RequestMappingInfo和MappingRegistration的對(duì)應(yīng)關(guān)系~
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
// 對(duì)于保存著mapping和HandlerMethod的對(duì)應(yīng)關(guān)系~
//對(duì)于RequestMappingHandlerMapping來說
//保存著RequestMappingInfo和HandlerMethod的對(duì)應(yīng)關(guān)系~
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
// 這里的Map不是普通的Map,而是MultiValueMap,它是個(gè)多值Map。其實(shí)它的value是一個(gè)list類型的值
// 至于為何是多值?有這么一種情況 URL都是/api/v1/hello 但是有的是get post delete等方法 所以有可能是會(huì)匹配到多個(gè)MappingInfo的
//對(duì)于RequestMappingHandlerMapping來說,保存著URL和RequestMappingInfo的關(guān)系~
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
//對(duì)于RequestMappingHandlerMapping來說,保存著URL和HandlerMethod的關(guān)系~
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>()
// 這兩個(gè)就不用解釋了
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
// 讀寫鎖~~~ 讀寫分離 提高啟動(dòng)效率
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//5.1版本其子類只有一個(gè)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方法,子類實(shí)現(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();
}
}這個(gè)注冊中心,核心是保存了多個(gè)Map映射關(guān)系,相當(dāng)于緩存下來。在請(qǐng)求過來時(shí)需要查找的時(shí)候,可以迅速定位到處理器
在其初始化過程中,其主要模版化的2個(gè)方法
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在容器啟動(dòng)過程中初始化,把掃描到的handler放到注冊表中
private final MappingRegistry mappingRegistry = new MappingRegistry();
//忽略初始化部分代碼.....
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
//獲取請(qǐng)求路徑
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
//放到請(qǐng)求屬性中
request.setAttribute(LOOKUP_PATH, lookupPath);
this.mappingRegistry.acquireReadLock();
try {
//根據(jù)請(qǐng)求和路徑獲取對(duì)應(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是一個(gè)private class,內(nèi)部就兩個(gè)屬性:T mapping和HandlerMethod handlerMethod
List<Match> matches = new ArrayList<>();
// 根據(jù)lookupPath去注冊中心里查找RequestMappingInfo,因?yàn)橐粋€(gè)具體的url可能匹配上多個(gè)RequestMappingInfo
// 至于為何是多值?有這么一種情況 URL都是/api/v1/hello 但是有的是get post delete等方法 等不一樣,都算多個(gè)的 所以有可能是會(huì)匹配到多個(gè)MappingInfo的
// 所有這個(gè)里可以匹配出多個(gè)出來。比如/hello 匹配出GET、POST、PUT都成,所以size可以為3
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
// 依賴于子類實(shí)現(xiàn)的抽象方法:getMatchingMapping() 看看到底匹不匹配,而不僅僅是URL匹配就行
// 比如還有method、headers、consumes等等這些不同都代表著不同的MappingInfo的
// 最終匹配上的,會(huì)new Match()放進(jìn)matches里面去
addMatchingMappings(directPathMatches, matches, request);
}
// 當(dāng)還沒有匹配上的時(shí)候,別無選擇,只能瀏覽所有映射
// 這里為何要瀏覽所有的mappings呢?而不是報(bào)錯(cuò)404呢?
// 增加路徑匹配對(duì)范圍,如:/rest 匹配 /rest.ssss
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
// 只要找到了一個(gè)匹配的 就進(jìn)來這里了~~~
// 請(qǐng)注意:因?yàn)榈竭@里 匹配上的可能還不止一個(gè) 所以才需要繼續(xù)處理~~
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
//如果匹配到多個(gè),就取第一個(gè)
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);
//請(qǐng)求域增加一些屬性,子類重寫
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
//請(qǐng)求域增加一些屬性,子類重寫
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ù)請(qǐng)求獲取對(duì)應(yīng)的handlerMethod過程是:
1、獲取請(qǐng)求路徑
2、根據(jù)路徑到注冊表中查詢對(duì)應(yīng)路徑的RequestMappingInfo
3、如果匹配到多個(gè),就取第一個(gè)。
4、如果匹配不到,就到注冊表中查詢所有RequestMappingInfo,匹配規(guī)則我們可以自定義。
Spring MVC請(qǐng)求URL帶后綴匹配的情況,如/hello.json也能匹配/hello
RequestMappingInfoHandlerMapping 在處理http請(qǐng)求的時(shí)候, 如果 請(qǐng)求url 有后綴,如果找不到精確匹配的那個(gè)@RequestMapping方法。 那么,就把后綴去掉,然后.*去匹配,這樣,一般都可以匹配,默認(rèn)這個(gè)行為是被開啟的。
比如有一個(gè)@RequestMapping("/rest"), 那么精確匹配的情況下, 只會(huì)匹配/rest請(qǐng)求。 但如果我前端發(fā)來一個(gè) /rest.abcdef 這樣的請(qǐng)求, 又沒有配置 @RequestMapping("/rest.abcdef") 這樣映射的情況下, 那么@RequestMapping("/rest") 就會(huì)生效。
這樣會(huì)帶來什么問題呢?絕大多數(shù)情況下是沒有問題的,但是如果你是一個(gè)對(duì)權(quán)限要求非常嚴(yán)格的系統(tǒng),強(qiáng)烈關(guān)閉此項(xiàng)功能,否則你會(huì)有意想不到的"收獲"。
究其原因咱們可以接著上面的分析,其實(shí)就到了PatternsRequestCondition這個(gè)類上,具體實(shí)現(xiàn)是它的匹配邏輯來決定的。
public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
...
@Override
@Nullable
public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
// patterns表示此MappingInfo可以匹配的值們。一般對(duì)應(yīng)@RequestMapping注解上的patters數(shù)組的值
if (this.patterns.isEmpty()) {
return this;
}
// 拿到待匹配的值,比如此處為"/hello.json"
String lookupPath = this.pathHelper.getLookupPathForRequest(request);
// 最主要就是這個(gè)方法了,它拿著這個(gè)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) {
// 最最最重點(diǎn)就是在getMatchingPattern()這個(gè)方法里~~~ 拿著lookupPath和pattern看它倆合拍不~
String match = getMatchingPattern(pattern, lookupPath);
if (match != null) {
matches.add(match);
}
}
// 解釋一下為何匹配的可能是多個(gè)。因?yàn)閡rl匹配上了,但是還有可能@RequestMapping的其余屬性匹配不上啊,所以此處需要注意 是可能匹配上多個(gè)的 最終是唯一匹配就成~
if (matches.size() > 1) {
matches.sort(this.pathMatcher.getPatternComparator(lookupPath));
}
return matches;
}
// // ===============url的真正匹配規(guī)則 非常重要~~~===============
// 注意這個(gè)方法的取名,上面是負(fù)數(shù),這里是單數(shù)~~~~命名規(guī)范也是有藝術(shù)感的
@Nullable
private String getMatchingPattern(String pattern, String lookupPath) {
// 完全相等,那就不繼續(xù)聊了~~~
if (pattern.equals(lookupPath)) {
return pattern;
}
// 注意了:useSuffixPatternMatch 這個(gè)屬性就是我們最終要關(guān)閉后綴匹配的關(guān)鍵
// 這個(gè)值默外部給傳的true(其實(shí)內(nèi)部默認(rèn)值是boolean類型為false)
if (this.useSuffixPatternMatch) {
// 這個(gè)意思是若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就給你添加上后綴".*",表示匹配所有請(qǐng)求的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的寬容度是很高的,容錯(cuò)處理做得是非常不錯(cuò)的~~~~~~~
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)閉此項(xiàng)功能呢?
為何要關(guān)閉的理由,上面其實(shí)已經(jīng)說了。當(dāng)我們涉及到嚴(yán)格的權(quán)限校驗(yàn)(強(qiáng)權(quán)限控制)的時(shí)候。特備是一些銀行系統(tǒng)、資產(chǎn)系統(tǒng)等等,關(guān)閉后綴匹配事非常有必要的。
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware {
private boolean useSuffixPatternMatch = true;
private boolean useTrailingSlashMatch = true;
}可以看到這兩個(gè)屬性值都直接冒泡到RequestMappingHandlerMapping這個(gè)實(shí)現(xiàn)類上來了,所以我們直接通過配置來改變它的默認(rèn)行為就成。
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
// 關(guān)閉后綴名匹配,關(guān)閉最后一個(gè)/匹配
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
configurer.setUseTrailingSlashMatch(false);
}
}
}**就這么一下,我們的URL就安全了,再也不能后綴名任意匹配了。**在想用后綴匹配,就甩你四個(gè)大字:404
到此這篇關(guān)于SpringMVC處理器映射器HandlerMapping詳解的文章就介紹到這了,更多相關(guān)SpringMVC的HandlerMapping內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java線程并發(fā)控制同步工具CountDownLatch
這篇文章主要為大家介紹了java線程并發(fā)控制同步工具CountDownLatch使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
SpringBoot集成Poi-tl實(shí)現(xiàn)動(dòng)態(tài)Word文檔生成的詳細(xì)步驟
在項(xiàng)目開發(fā)過程中,遇到了一個(gè)需求:將用戶輸入的數(shù)據(jù)填充到給定格式的?Word?文檔中,簡單來說,就是要根據(jù)預(yù)定義的模板生成一個(gè)新的?Word?文檔,并自動(dòng)填充數(shù)據(jù),所以本文給大家介紹了SpringBoot集成Poi-tl實(shí)現(xiàn)動(dòng)態(tài)Word文檔生成的詳細(xì)步驟,需要的朋友可以參考下2025-07-07
SpringMVC中控制器返回JSON數(shù)據(jù)的實(shí)現(xiàn)
本文主要介紹了SpringMVC中控制器返回JSON數(shù)據(jù)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
Java Swing最詳細(xì)基礎(chǔ)知識(shí)總結(jié)
這篇文章主要介紹了Java Swing最詳細(xì)基礎(chǔ)知識(shí)總結(jié),文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)Java Swing的小伙伴們有很好的幫助,需要的朋友可以參考下2021-05-05
SpringBoot實(shí)現(xiàn)圖片上傳及本地訪問
在SpringBoot項(xiàng)目中,處理靜態(tài)文件訪問尤其是實(shí)時(shí)更新的文件如商品圖片,可通過配置WebMvcConfig將本地文件映射到URL路徑上,以解決重啟項(xiàng)目才能訪問文件的問題,本文詳解如何保存和訪問這些文件,幫助開發(fā)者優(yōu)化項(xiàng)目文件管理2022-09-09

