Spring 註解面面通 之 @RequestMapping params 條件匹配原始碼解析
技術標籤:Spring 全面解析SpringRequestMappingparams對映條件
@RequestMapping
支援基於value
、path
、method
、params
、headers
、consumers
、produces
的匹配,本文對基於params
的匹配過程進行分析。
系列博文《Spring 註解面面通 之 @RequestMapping 請求匹配處理方法原始碼解析》中對請求匹配@RequestMapping
註釋方法流程進行了分析。
AbstractHandlerMethodMapping.lookupHandlerMethod(...)
方法負責查詢請求最佳匹配的處理方法。ParamsRequestCondition
params
的匹配過程,可以配置多個引數條件,多個條件之間是邏輯與(&&
)的關係。
原始碼解析
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) ParamsRequestCondition.getMatchingCondition(...)
方法。
此方法僅遍歷當前對映配置的引數表示式,用其與請求進行匹配處理,只有在所有表示式都匹配成功的情況下,引數條件才認為匹配成功。
/**
* 若請求引數匹配當前對映的所有引數表示式時,返回當前條件表示式例項.
* 否則,返回null,表明不匹配.
*/
@Override
@Nullable
public ParamsRequestCondition getMatchingCondition(HttpServletRequest request) {
// 遍歷當前對映的所有引數表示式.
for (ParamExpression 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) ParamsRequestCondition.ParamExpression.matchName(...)
和 ParamsRequestCondition.ParamExpression.matchValue(...)
方法。
① 當前引數表示式下,進行引數名的匹配:
· 請求引數中是否包含對映配置的引數名。
· 前端通過submit
提交請求,並使用圖片代替submit
按鈕,此時會傳遞.x
和.y
結尾的引數,此時需要在對映配置的引數名後追加.x
和.y
進行匹配。
② 當前引數表示式下,進行引數名的匹配:
· 對映配置的引數值式與request.getParameter(...)
進行匹配。
· 對映配置的引數值與請求引數值不其一為null
,則匹配失敗。
· 匹配過程首先使用=
進行比較,然後使用equals
進行比較。
· 引數是Array型別時,需特殊處理,逐項比較陣列各索引位置的值。
/**
* 進行引數名的匹配.
*/
@Override
protected boolean matchName(HttpServletRequest request) {
// 請求是否包含對映配置的引數名.
// 注:前端通過submit提交請求,並使用圖片代替submit按鈕,此時會傳遞xxx.x和xxx.y的引數,判斷是需在name基礎上
// 追加".x"和".y"進行匹配.
return (WebUtils.hasSubmitParameter(request, this.name) ||
request.getParameterMap().containsKey(this.name));
}
/**
* 進行引數值的匹配.
*/
@Override
protected boolean matchValue(HttpServletRequest request) {
// 對映引數表示式與request.getParameter(...)進行匹配.
// 1.引數表示式的值與請求引數值不為null,否則匹配失敗.
// 2.引數首先使用=進行比較,然後使用equals進行比較.
// 3.若引數是Array型別,需逐項比較陣列各索引位置的值.
return ObjectUtils.nullSafeEquals(this.value, request.getParameter(this.name));
}
總結
@RequestMapping
的params
匹配過程不是很複雜,邏輯相對亦比較簡單。
原始碼解析基於spring-framework-5.0.5.RELEASE
版本原始碼。
若文中存在錯誤和不足,歡迎指正!