1. 程式人生 > 其它 >Spring 註解面面通 之 @RequestMapping params 條件匹配原始碼解析

Spring 註解面面通 之 @RequestMapping params 條件匹配原始碼解析

技術標籤:Spring 全面解析SpringRequestMappingparams對映條件

  @RequestMapping支援基於valuepathmethodparamsheadersconsumersproduces的匹配,本文對基於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));
}

  總結

  @RequestMappingparams匹配過程不是很複雜,邏輯相對亦比較簡單。

  原始碼解析基於spring-framework-5.0.5.RELEASE版本原始碼。

  若文中存在錯誤和不足,歡迎指正!