dispatcherServlet在匹配请求的时候,用到了HandlerMapping。然而handler是如何匹配这个url路径的呢。我们来分析一下。因为我们最常用的是用@RequestMapping注解来实现handler的。所以我们主要分析的是这种情况。
1、获取路径的总的处理逻辑
首先看handlerMapping的初始化。因为spring mvc默认初始化的HandlerMapping是这两个RequestMappingHandlerMapping和BeanNameUrlHandlerMapping。其中RequestMappingHandlerMapping是主要用来处理@RequestMapping注解的HandlerMapping。我们主要看这个。先看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这个成员变量来处理了。
//org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
// 所有匹配到的方法都将存储在这里
List<Match> matches = new ArrayList<>();
// 通过urlLookup属性中查找。如果找到了,就返回对应的RequestMappingInfo
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
// 如果匹配到了,检查其他属性是否符合要求。如请求方法,参数,header等
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// 没有直接匹配到,则讲所有的handler全部拉进来,进行通配符匹配
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
// 这里的逻辑主要用来处理如果方法有多个匹配,不同的通配符等。则排序选择出最合适的一个
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
// 这里用来处理如果两个方法不同,但通配符可以对一个url具有相同优先级的时候。就抛错。
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、路径匹配的具体过程
所有路径对应的处理信息是方法mappingRegistry中的,mappingRegistry是MappingRegistry类型的,是AbstractHandlerMethodMapping的内部类
这个类的内部构造是这样的。注意registry和urlLookUp这两个属性。registry类型是一个map,其中key是RequestMappingInfo类型。这个类型保存了处理这个方法的所有信息。包括所在的bean,方法名,方法参数,返回值以及注解。注解信息里面就包括路径,参数,请求方法等。而urlLookup里面存储的时候url路径和RequestMappingInfo对应的信息。它的类型是MultiValueMap类型。可以一个key对应多个值。这里主要是为了解决一个url路径对应多个请求方法的情况。它们的初始化是在AbstractHandlerMethodMapping bean被创建的时候初始化的。采用的方式是调用了spring的InitializingBean的逻辑进行初始化的。
注意:带有通配符的路径匹配不在urlLookup属性参数中,只存在了registry。直接匹配所有的参数路径中,才会两者都存。
//org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#addMatchingMappings
// 过滤请求的逻辑
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
for (T mapping : mappings) {
//查看这个方法是否匹配这个请求
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
// 校验当前的handler是否适合这个当前请求,主要匹配逻辑在getMatchingCondition中。
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
return info.getMatchingCondition(request);
}
我们从这里看到,过滤请求的主要逻辑在RequestMappingInfo 的getMatchingCondition中。我们再进去看看。
//org.springframework.web.servlet.mvc.method.RequestMappingInfo#getMatchingCondition
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
//首先匹配请求方法
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
//匹配请求参数
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
//匹配请求头
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
//匹配可以接受请求的数据类型
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
//匹配可以发送的响应类型
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
//上面任何一个没有匹配到都直接返回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 留下的扩展口,可以自定义匹配逻辑
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的时候,不仅可以用方法进行区分,还可以用参数,header,consumer,produce中的任何一个来加以分区调用不同的方法的。例如不想要某个参数,只需要用前面加上!即可。如@RequestMapping(param="!name")就表示匹配没有参数是name的所有请求。header也可以这么处理。还有匹配@RequestMapping(param="!name=张三")就是匹配所有name不等于张三的所有请求。这种表达式逻辑只有param和head有。其他几种都没有。
具体的匹配逻辑如下:
//匹配param是否符合表达式的处理逻辑。主要逻辑在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
//先匹配表达式有没有这个值,有的话先按照值的方式处理
//如果没有值,则匹配有没有名字
//最后匹配是不是反向选择,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);
}
匹配到了,就把当前的RequestMappingInfo 返回。表示匹配到这个条件。
3、对于多个请求匹配后的排序,获取最合适的那一个
我们回到最开始的获取到所有的匹配方法之后,还需要进行排序。MatchComparator是用于排序的比较器。
//主要对于多个请求的匹配之后的排序逻辑
// org.springframework.web.servlet.mvc.method.RequestMappingInfo#compareTo
public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
int result;
// Automatic vs explicit HTTP HEAD mapping
// 如果是head请求,则按照方法进行处理。
if (HttpMethod.HEAD.matches(request.getMethod())) {
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
}
//然后按照路径进行处理
result = this.patternsCondition.compareTo(other.getPatternsCondition(), request);
if (result != 0) {
return result;
}
//按照参数进行排序
result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
if (result != 0) {
return result;
}
//按照header进行排序
result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
if (result != 0) {
return result;
}
//按照consumes可以接受的参数进行排序
result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
if (result != 0) {
return result;
}
//按照produces可以接受的参数进行排序
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;
}
从这里我们看到,主要是先按照路径,然后是参数,然后是header进行排序的。方法反而是最后一个。所以在设计多个方法匹配相同带有通配符url的时候,应当优先按照参数处理,而不是方法。
4、总结
Spring mvc的路径处理很是复杂,但灵活性好。基本上涵盖了我们可以对某个路径来处理的所有方法了。这么多过滤方式,我们可以用它来实现更加复杂的业务逻辑处理。如果通过某个参数控制处理方法,通过请求头或者需要的响应数据的类型来控制处理方法等。都是可以的。
网友评论