Spring 註解面面通 之 @RequestMapping headers 條件匹配原始碼解析
技術標籤:Spring 全面解析SpringRequestMappingheaders對映條件
@RequestMapping
支援基於value
、path
、method
、params
、headers
、consumers
、produces
的匹配,本文對基於params
的匹配過程進行分析。
系列博文《Spring 註解面面通 之 @RequestMapping 請求匹配處理方法原始碼解析》中對請求匹配@RequestMapping
註釋方法流程進行了分析。
AbstractHandlerMethodMapping.lookupHandlerMethod(...)
方法負責查詢請求最佳匹配的處理方法。HeadersRequestCondition
headers
的匹配過程,可以配置多個標頭條件,多個條件之間是邏輯與(&&
)的關係。
原始碼解析
1) AbstractHandlerMethodMapping.lookupHandlerMethod(...)
方法。
① 在mappingRegistry.urlLookup
中查詢與請求路徑完全匹配的對映。
② 在①
中查詢結果,查詢與請求完全匹配的匹配器。
③ 若②
中查詢無結果,則遍歷註冊的所有對映,進行進一步匹配,以查詢合適的匹配器。
④ 若③
中查詢匹配器列表不為空,則從中通過MatchComparator
比較器選擇最優匹配器。
⑤ 若請求是有效的CORS
PREFLIGHT_AMBIGUOUS_MATCH
,即new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"))
。
⑥ 判斷③
中查詢匹配器列表,最優和次優匹配器是否一致,若兩者一致,則違背對映規則,丟擲異常。
⑦ 處理查詢到最優匹配器的情況,這步驟大致包括:儲存到最優匹配到請求屬性、解析URI模板變數,儲存到請求屬性、解析矩陣變數,儲存到請求屬性、解析可生成媒體型別,儲存到請求屬性。
⑧ 處理未查詢到匹配器的情況,這步驟大致包括:為確保無誤,再次進行匹配查詢、若仍無匹配結果,則丟擲異常。
/**
* 查詢請求最優匹配的處理方法.
* 如果找到多個處理方法,則選擇最優匹配的處理方法.
* @param lookupPath 在當前Servlet對映中對映查詢路徑.
* @param request 當前請求例項.
* @return 最優匹配的處理方法,如果沒有匹配處理方法,返回null.
*/
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
// 查詢與請求路徑完全匹配的對映.
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
// 查詢與請求完全匹配的匹配器.
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
// 若無完全匹配器,則需遍歷所有的對映,進行進一步匹配.
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
// 若查詢的匹配器列表不為空,則從中選擇最優的匹配器.
if (!matches.isEmpty()) {
// 獲取匹配比較器.
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
// 根據比較器對匹配器進行排序.
matches.sort(comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
}
// 取得第一個匹配器,用於選取最優匹配器.
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
// 如果請求是有效的CORS型別請求,返回指定的處理方法.
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();
throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
}
}
// 處理匹配器.
// 1.儲存到最優匹配到請求屬性.
// 2.解析URI模板變數,儲存到請求屬性.
// 3.解析矩陣變數,儲存到請求屬性.
// 4.解析可生成媒體型別,儲存到請求屬性.
handleMatch(bestMatch.mapping, lookupPath, request);
// 返回最優匹配器的處理方法.
return bestMatch.handlerMethod;
}
else {
// 處理無匹配器的情況.
// 1.再次進行匹配查詢.
// 2.若仍無匹配,丟擲異常.
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
2) AbstractHandlerMethodMapping.addMatchingMappings(...)
-> RequestMappingInfoHandlerMapping.getMatchingMapping(...)
-> RequestMappingInfo.getMatchingCondition(...)
。
AbstractHandlerMethodMapping.addMatchingMappings(...)
-> RequestMappingInfoHandlerMapping.getMatchingMapping(...)
-> RequestMappingInfo.getMatchingCondition(...)
中處理的中間流程,其中並沒有涉及過多步驟,在此不做深入分析。
3) HeadersRequestCondition.getMatchingCondition(...)
方法。
① 如果請求是有效的CORS
型別請求,則返回PRE_FLIGHT_MATCH
,即new HeadersRequestCondition()
。
② 遍歷當前對映配置的標頭表示式,用其與請求進行匹配處理,只有在所有表示式都匹配成功的情況下,標頭條件才認為匹配成功。
/**
* 若請求標頭匹配當前對映的所有標頭表示式時,返回當前條件表示式例項.
* 否則,返回null,表明不匹配.
*/
@Override
@Nullable
public HeadersRequestCondition getMatchingCondition(HttpServletRequest request) {
// 如果請求是有效的CORS型別請求,直接返回空的HeadersRequestCondition.
if (CorsUtils.isPreFlightRequest(request)) {
return PRE_FLIGHT_MATCH;
}
// 遍歷當前對映的所有標頭表示式.
for (HeaderExpression expression : expressions) {
if (!expression.match(request)) {
return null;
}
}
return this;
}
4) AbstractNameValueExpression.match(...)
方法。
① 當前標頭表示式下,進行標頭值的匹配。
② 當前標頭表示式下,進行標頭名的匹配。
③ isNegated
表示是否使用了!
的否定操作,根據其對①②
中的匹配結果進行轉換操作。
/**
* 請求標頭匹配單個標頭表示式.
*/
public final boolean match(HttpServletRequest request) {
boolean isMatch;
// 進行標頭值的匹配.
if (this.value != null) {
isMatch = matchValue(request);
}
// 進行標頭名的匹配.
else {
isMatch = matchName(request);
}
// 是否使用!來表達否定操作.
return (this.isNegated ? !isMatch : isMatch);
}
5) HeadersRequestCondition.HeaderExpression.matchName(...)
和 HeadersRequestCondition.HeaderExpression.matchValue(...)
方法。
① 當前標頭表示式下,進行標頭值的匹配:
· 判斷請求標頭中是否包含對映配置的標頭名。
② 當前標頭表示式下,進行標頭名的匹配:
· 對映配置的標頭值式與request.getHeader(...)
進行匹配。
· 對映配置的標頭值與請求標頭值不其一為null
,則匹配失敗。
· 匹配過程首先使用=
進行比較,然後使用equals
進行比較。
· 標頭值是Array型別時,需特殊處理,逐項比較陣列各索引位置的值。
/**
* 進行標頭名的匹配.
*/
@Override
protected boolean matchName(HttpServletRequest request) {
// 判斷請求標頭中是否包含對映配置的標頭名.
return (request.getHeader(this.name) != null);
}
/**
* 進行標頭值的匹配.
*/
@Override
protected boolean matchValue(HttpServletRequest request) {
// 對映配置的標頭表示式與request.getHeader(...)進行匹配.
// 1.標頭表示式的值與請求標頭值不為null,否則匹配失敗.
// 2.標頭首先使用=進行比較,然後使用equals進行比較.
// 3.若標頭值是Array型別,需逐項比較陣列各索引位置的值.
return ObjectUtils.nullSafeEquals(this.value, request.getHeader(this.name));
}
總結
@RequestMapping
的headers
匹配過程不是很複雜,邏輯相對亦比較簡單。
原始碼解析基於spring-framework-5.0.5.RELEASE
版本原始碼。
若文中存在錯誤和不足,歡迎指正!