Spring mvc url的匹配handler流程总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,222评论 1 92
  • 今天学习查找和替换的不同玩法,越是碎片化时代越需要系统性学习。已坚持打卡4天,养成了每天上午打卡的好习惯,同时有效...
    余峰_89b0阅读 153评论 0 0
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,678评论 6 342
  • 你还是你 我还是我 其实都没变 一直深爱对方 只是换种方式 多想回到从前 一觉醒来 我们如初识 初心给予你 默默深...
    英子丫头阅读 161评论 0 5