上篇文章裡,我們講解了RequestMappingHandlerMapping
請求地址對映的初始化流程,理解了@Controller
和@RequestMapping
是如何被載入到快取中的。
今天我們來進一步學習,在接收到請求時,RequestMappingHandlerMapping
是如何進行請求地址對映的。
先放一個類圖,在請求地址對映過程中,會依次執行到這些方法:
講解之前,先總結RequestMappingHandlerMapping
的請求地址對映流程:
- 獲取
handler
- 解析
request
,獲取請求路徑path
- 根據
path
查詢pathLookup
快取,獲取路徑匹配的RequestMappingInfo
列表 - 對上述
RequestMappingInfo
列表進行篩選,獲取條件匹配的RequestMappingInfo
列表 - 對上述
RequestMappingInfo
列表進行排序,獲取匹配度最高的RequestMappingInfo
- 根據上述
RequestMappingInfo
,獲取對應MappingRegistration
的HandlerMethod
作為handler
返回
- 解析
- 建立
HandlerExecutionChain
物件 - 新增配置攔截器
- 新增跨域攔截器
1 HandlerMapping
首先,DispatcherServlet
會呼叫HandlerMapping
介面的getHandler()
方法:
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
這個方法主要起著規範的作用,DispatcherServlet
可以根據這個方法呼叫所有HandlerMapping
實現類進行請求地址對映。
2 AbstractHandlerMapping
AbstractHandlerMapping
是所有HandlerMapping
的抽象基類,提供了攔截器、排序和預設處理器等功能。
AbstractHandlerMapping
是常見HandlerMapping
實現類的共同父類,它的核心功能是定義了獲取HandlerExecutionChain
的基礎流程:
- 獲取
handler
(由實現類定義具體邏輯) - 建立
HandlerExecutionChain
,新增攔截器 - 新增跨域攔截器
AbstractHandlerMapping
的getHandler()
原始碼如下:
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 1、獲取handler
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);
}
// 2、建立HandlerExecutionChain,新增攔截器
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (logger.isTraceEnabled()) {
logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
logger.debug("Mapped to " + executionChain.getHandler());
}
// 3、新增跨域攔截器
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;
}
2.1 獲取handler
AbstractHandlerMapping
透過getHandlerInternal()
方法獲取handler
。
該方法由具體實現類進行實現,如果找到匹配的handler
,則會返回該handler
;如果沒有找到,則會返回null
。
具體實現我們會在下文的實現類中進行講解。
2.2 建立HandlerExecutionChain,新增攔截器
AbstractHandlerMapping
透過getHandlerExecutionChain()
方法建立HandlerExecutionChain
物件,並新增攔截器。原始碼如下:
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
// 1、建立HandlerExecutionChain物件
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
// 2、新增攔截器
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;
}
它會對初始化時配置的攔截器進行遍歷:
- 如果是
MappedInterceptor
實現類,會根據匹配規則進行判斷是否新增。 - 如果不是
MappedInterceptor
實現類,會直接新增。
2.3 新增跨域攔截器
新增跨域攔截器分為以下幾個步驟:
- 判斷是否存在跨域配置,或是否預檢請求
- 獲取
handler
級別的跨域配置 - 獲取
HandlerMapping
級別的跨域配置 - 整合跨域配置
- 建立並新增跨域攔截器
2.3.1 判斷是否存在跨域配置
在AbstractHandlerMapping
中,會判斷handler
是否CorsConfigurationSource
的實現類(對於RequestMappingHandlerMapping
而言,handler
是HandlerMethod
型別,所以第一個條件永遠是false
),以及是否存在HandlerMapping
級別的跨域配置源:
protected boolean hasCorsConfigurationSource(Object handler) {
if (handler instanceof HandlerExecutionChain) {
handler = ((HandlerExecutionChain) handler).getHandler();
}
return (handler instanceof CorsConfigurationSource || this.corsConfigurationSource != null);
}
而在AbstractHandlerMethodMapping
子抽象類中,會進一步判斷是否存在handler
級別(也就是@CrossOrigin
級別)的跨域配置:
protected boolean hasCorsConfigurationSource(Object handler) {
return super.hasCorsConfigurationSource(handler) ||
(handler instanceof HandlerMethod &&
this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null);
}
2.3.2 判斷是否是預檢請求
org.springframework.web.cors.CorsUtils#isPreFlightRequest
:
public static boolean isPreFlightRequest(HttpServletRequest request) {
return (HttpMethod.OPTIONS.matches(request.getMethod()) &&
request.getHeader(HttpHeaders.ORIGIN) != null &&
request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
}
2.3.3 獲取handler級別跨域配置
在AbstractHandlerMapping
中,會判斷handler
是否CorsConfigurationSource
的實現類,從中獲取handler
級別的跨域配置。對於RequestMappingHandlerMapping
而言,handler
是HandlerMethod
型別,所以第一個條件永遠返回null
:
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
Object resolvedHandler = handler;
if (handler instanceof HandlerExecutionChain) {
resolvedHandler = ((HandlerExecutionChain) handler).getHandler();
}
if (resolvedHandler instanceof CorsConfigurationSource) {
return ((CorsConfigurationSource) resolvedHandler).getCorsConfiguration(request);
}
return null;
}
在AbstractHandlerMethodMapping
子抽象類中,會從mappingRegistry
(request-handler
快取)中獲取handler
級別的跨域配置(在上篇文章中,我們有講述過RequestMappingHandlerMapping
如何快取@CrossOrigin
級別的跨域配置的):
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request);
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) {
return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG;
}
else {
CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod);
corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod);
}
}
return corsConfig;
}
2.3.4 獲取HandlerMapping級別的跨域配置
從AbstractHandlerMapping
的corsConfigurationSource
成員變數中,可以獲取到HandlerMapping
級別的跨域配置,該配置可以透過以下方式新增:
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 新增HandlerMapping級別的跨域配置
}
}
2.3.5 整合跨域配置
在整合跨域配置過程中,有三種情況:
- 對於
origins
、originPatterns
、allowedHeaders
、exposedHeaders
和methods
等列表屬性,會獲取全部。 - 對於
allowCredentials
,會優先獲取方法級別的配置。 - 對於
maxAge
,會獲取最大值。
具體可以檢視相關原始碼:
public CorsConfiguration combine(@Nullable CorsConfiguration other) {
if (other == null) {
return this;
}
// Bypass setAllowedOrigins to avoid re-compiling patterns
CorsConfiguration config = new CorsConfiguration(this);
List<String> origins = combine(getAllowedOrigins(), other.getAllowedOrigins());
List<OriginPattern> patterns = combinePatterns(this.allowedOriginPatterns, other.allowedOriginPatterns);
config.allowedOrigins = (origins == DEFAULT_PERMIT_ALL && !CollectionUtils.isEmpty(patterns) ? null : origins);
config.allowedOriginPatterns = patterns;
config.setAllowedMethods(combine(getAllowedMethods(), other.getAllowedMethods()));
config.setAllowedHeaders(combine(getAllowedHeaders(), other.getAllowedHeaders()));
config.setExposedHeaders(combine(getExposedHeaders(), other.getExposedHeaders()));
Boolean allowCredentials = other.getAllowCredentials();
if (allowCredentials != null) {
config.setAllowCredentials(allowCredentials);
}
Long maxAge = other.getMaxAge();
if (maxAge != null) {
config.setMaxAge(maxAge);
}
return config;
}
2.3.6 建立並新增跨域攔截器
在這一步,對於預檢請求,會建立HandlerExecutionChain
;對於普通請求,會建立CorsInterceptor
攔截器,並新增到首位:
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
return new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
else {
chain.addInterceptor(0, new CorsInterceptor(config));
return chain;
}
}
3 AbstractHandlerMethodMapping
AbstractHandlerMethodMapping
是HandlerMethod
請求對映的抽象基類,它的getHandlerInternal()
方法定義了請求地址對映的核心流程:
- 解析請求路徑
- 根據請求地址查詢
HandlerMethod
AbstractHandlerMethodMapping#getHandlerInternal
:
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
// 1、解析請求地址
String lookupPath = initLookupPath(request);
this.mappingRegistry.acquireReadLock();
try {
// 2、根據請求地址查詢HandlerMethod
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
3.1 解析請求路徑
解析請求路徑過程會獲取當前請求的介面地址路徑。
簡單來說,會去除請求地址開頭的contextPaht
。例如在application.properties
配置contextPath
如下:
server.servlet.context-path=/context-path
此時,請求/context-path/test
地址,經過initLookPath()
方法處理,會返回/test
為實際請求路徑。
實際上,這也很容易理解。因為在RequestMappingHandlerMapping
初始化pathLookup
對映快取時,就沒有將contextPath
考慮在內,那麼在實際處理請求時,當然也要把contextPath
去掉。
解析請求路徑的作用也是為了方便直接從pathLookup
對映快取中獲取對應的RequestMappingInfo
資訊。
AbstractHandlerMapping#initLookupPath
原始碼如下:
protected String initLookupPath(HttpServletRequest request) {
if (usesPathPatterns()) {
request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
String lookupPath = requestPath.pathWithinApplication().value();
return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
}
else {
return getUrlPathHelper().resolveAndCacheLookupPath(request);
}
}
3.2 根據請求路徑查詢HandlerMethod
在AbstractHandlerMethodMapping#lookupHandlerMethod
方法中,會按如下步驟獲取HandlerMethod
:
- 根據請求路徑從
pathLookup
對映快取查詢對應的RequestMappingInfo
列表。 - 根據
RequestMappingInfo
從registry
快取中獲取對應的MappingRegistration
列表。 - 根據當前
request
,對MappingRegistration
列表按匹配度進行排序。 - 從中取匹配度最高的
HandlerMethod
進行返回。
AbstractHandlerMethodMapping#lookupHandlerMethod
原始碼如下:
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 (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
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);
}
}
3.2.1 查詢pathLookup快取
在RequestMappingHandlerMapping
請求地址對映的初始化過程中,會將@RequestMapping
中的資訊快取到pathLookup
中,其中該註解的請求路徑作為key
,該註解的各屬性封裝成RequestMappingInfo
作為值。
需要注意的是,pathLookup
的型別是MultiValueMap<String, T>
,這裡的T
就是RequestMappingInfo
。
pathLookup
的底層資料結構實際上是path-List<RequestMappingInfo>
,這是因為請求路徑不是介面的唯一指標,還包括請求頭、請求方法等資訊。
所以,一個請求地址實際上可能對映著多個HandlerMethod
。
例如,我們可以定義如下介面:
@RestController
public class SamePathController {
@GetMapping("/samePath")
public String get() {
return "get";
}
@PostMapping("/samePath")
public String post() {
return "post";
}
}
此時,GET localhost:8080/samePath
和POST localhost:8080/samePath
可以分別請求到對應的介面。
回到AbstractHandlerMethodMapping#getHandlerInternal
原始碼,此時透過請求路徑可以獲取多個RequestMappingInfo
:
List<RequestMappingInfo> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
3.2.2 查詢registry快取
在RequestMappingHandlerMapping
請求地址對映的初始化過程中,會將介面的詳細資訊快取到registry
中,將上述RequestMappingInfo
作為key
,將RequestMappingInfo
和HanlderMethod
等資訊裝成MappingRegistration
作為值。
registry
的型別是Map<T, MappingRegistration<T>>
,這裡的T
指的是RequestMappingInfo
。
需要注意的是,由於RequestMappingInfo
根據介面的@RequestMapping
資訊進行構造,如果存在@RequestMapping
資訊完全相同的多個介面,專案是無法啟動的。
因此,RequestMappingInfo
可以唯一定位到該介面,即RequestMappingInfo
和MappingRegistration
是一一對應的。我們也可以將RequestMappingInfo
等效於實際介面。
我們可以總結一下pathLookup
和registry
快取的關係:
回到AbstractHandlerMethodMapping#getHandlerInternal
原始碼:
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}
存在兩種情況:
- 如果在
pathLookup
快取中找到對應List<RequestMappingInfo>
,會進一步從該列表中查詢更加匹配的RequestMappingInfo
,並根據該RequestMapping
從registry
快取中找到對應的MappingRegistration
,封裝成Match
物件返回。 - 如果在
pathLookup
快取中沒有找到對應List<RequestMappingInfo>
,會遍歷registry
快取中的所有key
,從中查詢更加匹配的RequestMappingInfo
,並根據該RequestMapping
從registry
快取中找到對應的MappingRegistration
,封裝成Match
物件返回。
具體流程對應的AbstractHandlerMethodMapping#addMatchingMappings
原始碼如下:
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
for (T mapping : mappings) {
T match = getMatchingMapping(mapping, request);
if (match != null) {
matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
}
}
}
查詢更加匹配的RequestMappingInfo
對應的是RequestMappingInfoHandlerMapping#getMatchingMapping
方法:
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
return info.getMatchingCondition(request);
}
RequestMappingInfo#getMatchingCondition
方法會對請求的methods
、params
、consumes
、produces
以及path
進行校驗,只有所有條件透過才會返回該RequestMappingInfo
,否則會返回null
。具體原始碼如下:
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
if (methods == null) {
return null;
}
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
if (params == null) {
return null;
}
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
if (headers == null) {
return null;
}
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
if (consumes == null) {
return null;
}
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (produces == null) {
return null;
}
PathPatternsRequestCondition pathPatterns = null;
if (this.pathPatternsCondition != null) {
pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
if (pathPatterns == null) {
return null;
}
}
PatternsRequestCondition patterns = null;
if (this.patternsCondition != null) {
patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {
return null;
}
}
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null) {
return null;
}
return new RequestMappingInfo(this.name, pathPatterns, patterns,
methods, params, headers, consumes, produces, custom, this.options);
}
通常情況下,透過這種判斷可以篩選出唯一一個對應的RequestMappingInfo
,除非是我們定義的介面比較特殊。
例如,我們定義介面如下:
@RestController
public class SamePathController {
@RequestMapping(value = "samePath", method = {RequestMethod.GET, RequestMethod.POST})
public String getAndPost() {
return "getAndPost";
}
@PostMapping("/samePath")
public String post() {
return "post";
}
}
此時,請求GET localhost:8080/samePath
,可以篩選出來唯一一個定位到getAndPost()
介面的RequestMappingInfo
;請求POST localhost:8080/samePath
,值可以篩選出兩個分別定義到getAndPost()
和post()
方法的RequestMappingInfo
,因為它們的規則都滿足條件,需要進一步篩選。
3.2.3 按匹配度排序
通常情況下,透過上述步驟可以篩選出唯一一個RequestMappingInfo
。
但是也有可能定義出條件重疊的介面(不推薦),此時會篩選出多個RequestMappingInfo
。此時,需要根據某種規則進行匹配度排序。
RequestMappingInfo
對於匹配度排序的規則是:
- 比較
methods
、params
和headers
等條件的長度:越短越具體,匹配度越高。 - 長度相等時,比較其他特殊規則:例如
methods
包含HEAD
方法的匹配度高。
具體實現原始碼在RequestMappingInfo#compareTo
:
public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
int result;
// Automatic vs explicit HTTP HEAD mapping
if (HttpMethod.HEAD.matches(request.getMethod())) {
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
}
result = getActivePatternsCondition().compareTo(other.getActivePatternsCondition(), request);
if (result != 0) {
return result;
}
result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
if (result != 0) {
return result;
}
result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
if (result != 0) {
return result;
}
result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
if (result != 0) {
return result;
}
result = this.producesCondition.compareTo(other.getProducesCondition(), request);
if (result != 0) {
return result;
}
// Implicit (no method) vs explicit HTTP method mappings
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
result = this.customConditionHolder.compareTo(other.customConditionHolder, request);
if (result != 0) {
return result;
}
return 0;
}
3.2.4 獲取匹配度最高的HandlerMethod
透過上述步驟,我們最終獲取到匹配度最高的RequestMappingInfo
,直接取對應MappingRegistration
的HandlerMethod
成員變數返回即可。