Spring mvc url的匹配handler流程总结

2018-11-17  本文已影响0人  元勰

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的路径处理很是复杂,但灵活性好。基本上涵盖了我们可以对某个路径来处理的所有方法了。这么多过滤方式,我们可以用它来实现更加复杂的业务逻辑处理。如果通过某个参数控制处理方法,通过请求头或者需要的响应数据的类型来控制处理方法等。都是可以的。

上一篇 下一篇

猜你喜欢

热点阅读