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ù)的類型來控制處理方法等。都是可以的更舞。