Spring mvc url的匹配handler流程總結(jié)

dispatcherServlet在匹配請求的時(shí)候,用到了HandlerMapping。然而handler是如何匹配這個(gè)url路徑的呢舞虱。我們來分析一下睦番。因?yàn)槲覀冏畛S玫氖怯聾RequestMapping注解來實(shí)現(xiàn)handler的类茂。所以我們主要分析的是這種情況耍属。

1、獲取路徑的總的處理邏輯

首先看handlerMapping的初始化巩检。因?yàn)閟pring mvc默認(rèn)初始化的HandlerMapping是這兩個(gè)RequestMappingHandlerMapping和BeanNameUrlHandlerMapping厚骗。其中RequestMappingHandlerMapping是主要用來處理@RequestMapping注解的HandlerMapping。我們主要看這個(gè)兢哭。先看RequestMappingHandlerMapping是怎么查找url路徑的领舰。
在父類AbstractHandlerMethodMapping中,通過url來查找的迟螺。一下就是獲取url路徑和查找獲取方法的邏輯冲秽。那么主要邏輯在查找方法lookupHandlerMethod中

/**
    * Look up a handler method for the given request.
    */
     //org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal
   @Override
   protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
       String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
       this.mappingRegistry.acquireReadLock();
       try {
           HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
           return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
       }
       finally {
           this.mappingRegistry.releaseReadLock();
       }
   }

我們看lookupHandlerMethod的邏輯。主要邏輯委托給了mappingRegistry這個(gè)成員變量來處理了矩父。

//org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    // 所有匹配到的方法都將存儲在這里
    List<Match> matches = new ArrayList<>();
    // 通過urlLookup屬性中查找锉桑。如果找到了,就返回對應(yīng)的RequestMappingInfo
    List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
    if (directPathMatches != null) {
         // 如果匹配到了窍株,檢查其他屬性是否符合要求民轴。如請求方法,參數(shù)球订,header等
        addMatchingMappings(directPathMatches, matches, request);
    }
    if (matches.isEmpty()) {
        // 沒有直接匹配到杉武,則講所有的handler全部拉進(jìn)來,進(jìn)行通配符匹配
        addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
    }
 
    if (!matches.isEmpty()) {
        // 這里的邏輯主要用來處理如果方法有多個(gè)匹配辙售,不同的通配符等轻抱。則排序選擇出最合適的一個(gè)
        Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
        matches.sort(comparator);
        Match bestMatch = matches.get(0);
        if (matches.size() > 1) {
             // 這里用來處理如果兩個(gè)方法不同,但通配符可以對一個(gè)url具有相同優(yōu)先級的時(shí)候旦部。就拋錯(cuò)祈搜。
            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(
                        "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
            }
        }
        handleMatch(bestMatch.mapping, lookupPath, request);
        return bestMatch.handlerMethod;
    }
    else {
        return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
    }
}

2、路徑匹配的具體過程

所有路徑對應(yīng)的處理信息是方法mappingRegistry中的士八,mappingRegistry是MappingRegistry類型的容燕,是AbstractHandlerMethodMapping的內(nèi)部類

這個(gè)類的內(nèi)部構(gòu)造是這樣的。注意registry和urlLookUp這兩個(gè)屬性婚度。registry類型是一個(gè)map蘸秘,其中key是RequestMappingInfo類型。這個(gè)類型保存了處理這個(gè)方法的所有信息蝗茁。包括所在的bean醋虏,方法名,方法參數(shù)哮翘,返回值以及注解颈嚼。注解信息里面就包括路徑,參數(shù)饭寺,請求方法等阻课。而urlLookup里面存儲的時(shí)候url路徑和RequestMappingInfo對應(yīng)的信息叫挟。它的類型是MultiValueMap類型∠奚罚可以一個(gè)key對應(yīng)多個(gè)值抹恳。這里主要是為了解決一個(gè)url路徑對應(yīng)多個(gè)請求方法的情況。它們的初始化是在AbstractHandlerMethodMapping bean被創(chuàng)建的時(shí)候初始化的署驻。采用的方式是調(diào)用了spring的InitializingBean的邏輯進(jìn)行初始化的奋献。
注意:帶有通配符的路徑匹配不在urlLookup屬性參數(shù)中,只存在了registry硕舆。直接匹配所有的參數(shù)路徑中秽荞,才會兩者都存。

//org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#addMatchingMappings
// 過濾請求的邏輯
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
    for (T mapping : mappings) {
        //查看這個(gè)方法是否匹配這個(gè)請求
        T match = getMatchingMapping(mapping, request);
        if (match != null) {
            matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
        }
    }
}
 
/**
* Check if the given RequestMappingInfo matches the current request and
* return a (potentially new) instance with conditions that match the
* current request -- for example with a subset of URL patterns.
* @return an info in case of a match; or {@code null} otherwise.
*/
//org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getMatchingMapping
// 校驗(yàn)當(dāng)前的handler是否適合這個(gè)當(dāng)前請求抚官,主要匹配邏輯在getMatchingCondition中扬跋。
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
    return info.getMatchingCondition(request);
}

我們從這里看到,過濾請求的主要邏輯在RequestMappingInfo 的getMatchingCondition中凌节。我們再進(jìn)去看看钦听。

//org.springframework.web.servlet.mvc.method.RequestMappingInfo#getMatchingCondition
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
    //首先匹配請求方法
    RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
    //匹配請求參數(shù)
    ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
    //匹配請求頭
    HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
    //匹配可以接受請求的數(shù)據(jù)類型
    ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
    //匹配可以發(fā)送的響應(yīng)類型
    ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
    //上面任何一個(gè)沒有匹配到都直接返回null,表示沒有匹配
    if (methods == null || params == null || headers == null || consumes == null || produces == null) {
        return null;
    }
 
    //查詢url路徑的匹配
    PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
    if (patterns == null) {
        return null;
    }
    //spring 留下的擴(kuò)展口倍奢,可以自定義匹配邏輯
    RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
    if (custom == null) {
        return null;
    }
 
    return new RequestMappingInfo(this.name, patterns,
            methods, params, headers, consumes, produces, custom.getCondition());
}

這里說明了朴上,我們在編寫handler的時(shí)候,不僅可以用方法進(jìn)行區(qū)分卒煞,還可以用參數(shù)痪宰,header,consumer畔裕,produce中的任何一個(gè)來加以分區(qū)調(diào)用不同的方法的衣撬。例如不想要某個(gè)參數(shù),只需要用前面加上!即可扮饶。如@RequestMapping(param="!name")就表示匹配沒有參數(shù)是name的所有請求具练。header也可以這么處理。還有匹配@RequestMapping(param="!name=張三")就是匹配所有name不等于張三的所有請求甜无。這種表達(dá)式邏輯只有param和head有扛点。其他幾種都沒有。
具體的匹配邏輯如下:

//匹配param是否符合表達(dá)式的處理邏輯岂丘。主要邏輯在match中
//org.springframework.web.servlet.mvc.condition.ParamsRequestCondition#getMatchingCondition
public ParamsRequestCondition getMatchingCondition(HttpServletRequest request) {
    for (ParamExpression expression : this.expressions) {
        if (!expression.match(request)) {
            return null;
        }
    }
    return this;
}
//org.springframework.web.servlet.mvc.condition.AbstractNameValueExpression#match
//先匹配表達(dá)式有沒有這個(gè)值陵究,有的話先按照值的方式處理
//如果沒有值,則匹配有沒有名字
//最后匹配是不是反向選擇元潘,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);
}

匹配到了,就把當(dāng)前的RequestMappingInfo 返回翩概。表示匹配到這個(gè)條件牲距。

3、對于多個(gè)請求匹配后的排序钥庇,獲取最合適的那一個(gè)

我們回到最開始的獲取到所有的匹配方法之后牍鞠,還需要進(jìn)行排序。MatchComparator是用于排序的比較器评姨。

//主要對于多個(gè)請求的匹配之后的排序邏輯
// org.springframework.web.servlet.mvc.method.RequestMappingInfo#compareTo
public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
    int result;
    // Automatic vs explicit HTTP HEAD mapping
    // 如果是head請求难述,則按照方法進(jìn)行處理。
    if (HttpMethod.HEAD.matches(request.getMethod())) {
        result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
        if (result != 0) {
            return result;
        }
    }
    //然后按照路徑進(jìn)行處理
    result = this.patternsCondition.compareTo(other.getPatternsCondition(), request);
    if (result != 0) {
        return result;
    }
    //按照參數(shù)進(jìn)行排序
    result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
    if (result != 0) {
        return result;
    }
    //按照header進(jìn)行排序
    result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
    if (result != 0) {
        return result;
    }
    //按照consumes可以接受的參數(shù)進(jìn)行排序
    result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
    if (result != 0) {
        return result;
    }
    //按照produces可以接受的參數(shù)進(jìn)行排序
    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;
}

從這里我們看到胁后,主要是先按照路徑,然后是參數(shù)嗦枢,然后是header進(jìn)行排序的攀芯。方法反而是最后一個(gè)。所以在設(shè)計(jì)多個(gè)方法匹配相同帶有通配符url的時(shí)候文虏,應(yīng)當(dāng)優(yōu)先按照參數(shù)處理侣诺,而不是方法。

4氧秘、總結(jié)

Spring mvc的路徑處理很是復(fù)雜年鸳,但靈活性好⊥柘啵基本上涵蓋了我們可以對某個(gè)路徑來處理的所有方法了搔确。這么多過濾方式,我們可以用它來實(shí)現(xiàn)更加復(fù)雜的業(yè)務(wù)邏輯處理灭忠。如果通過某個(gè)參數(shù)控制處理方法膳算,通過請求頭或者需要的響應(yīng)數(shù)據(jù)的類型來控制處理方法等。都是可以的更舞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末畦幢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子缆蝉,更是在濱河造成了極大的恐慌宇葱,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刊头,死亡現(xiàn)場離奇詭異黍瞧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)原杂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門印颤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人穿肄,你說我怎么就攤上這事年局〖士矗” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵矢否,是天一觀的道長仲闽。 經(jīng)常有香客問我,道長僵朗,這世上最難降的妖魔是什么赖欣? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮验庙,結(jié)果婚禮上顶吮,老公的妹妹穿的比我還像新娘。我一直安慰自己粪薛,他們只是感情好悴了,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著汗菜,像睡著了一般让禀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上陨界,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天巡揍,我揣著相機(jī)與錄音,去河邊找鬼菌瘪。 笑死腮敌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的俏扩。 我是一名探鬼主播糜工,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼录淡!你這毒婦竟也來了捌木?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嫉戚,失蹤者是張志新(化名)和其女友劉穎刨裆,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彬檀,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡帆啃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窍帝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片努潘。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出疯坤,到底是詐尸還是另有隱情报慕,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布贴膘,位于F島的核電站卖子,受9級特大地震影響略号,放射性物質(zhì)發(fā)生泄漏刑峡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一玄柠、第九天 我趴在偏房一處隱蔽的房頂上張望突梦。 院中可真熱鬧,春花似錦羽利、人聲如沸宫患。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娃闲。三九已至,卻和暖如春匾浪,著一層夾襖步出監(jiān)牢的瞬間皇帮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工蛋辈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留属拾,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓冷溶,卻偏偏與公主長得像渐白,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子逞频,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理纯衍,服務(wù)發(fā)現(xiàn),斷路器苗胀,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架襟诸,建立于...
    Hsinwong閱讀 22,403評論 1 92
  • 今天學(xué)習(xí)查找和替換的不同玩法,越是碎片化時(shí)代越需要系統(tǒng)性學(xué)習(xí)柒巫。已堅(jiān)持打卡4天励堡,養(yǎng)成了每天上午打卡的好習(xí)慣,同時(shí)有效...
    余峰_89b0閱讀 157評論 0 0
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,811評論 6 342
  • 你還是你 我還是我 其實(shí)都沒變 一直深愛對方 只是換種方式 多想回到從前 一覺醒來 我們?nèi)绯踝R 初心給予你 默默深...
    英子丫頭閱讀 162評論 0 5