欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringMVC 5.0 请求映射匹配核心源码解读 (SpringMVC 5.x 与 SpringMVC 4.x 对比)

程序员文章站 2022-06-15 19:36:43
...

本文出处:
深入理解SpringMVC 5.0 —— 请求映射匹配核心源码解读(SpringMVC 5.x 与 SpringMVC 4.x 对比)
https://blog.csdn.net/whdxjbw/article/details/81263922

书写原则

精炼、深刻、通俗,基于最新的SpringMVC 5.x版本,源码中加入大量注释分析,结合文字描述,实时比对新旧版本的区别,细致分析SpringMVC中请求匹配原理。

写在前面

  • 现在讲解SpringMVC的请求匹配原理的博文都是基于SpringMVC 4.x 或者更低的版本,所以如果你之前学习过类似的博文你会对urlMap很熟悉。但SpringMVC 5.x 中就不再是这样了,而且目前好像也没有对 5.x 版本的源码分析,这也是我为何写这篇博文的原因。
  • 本文书写时,SpringMVC最新版本为 5.0.7,在研究过程中,我发现 5.x 的版本和 4.x 的版本有的地方差别还挺大的,如引入了新的Java语法特性、功能更加细化、逻辑更加简单清晰、不再维护urlMap等,后面会一一讲述。

  • 现在有很多写SpringMVC整体原理的博文,大家如果第一次听说SpringMVC这个东西,建议还是先看看类似的博文,先从整体意识上对它有一个认识。

总的来说,本文从SpringMVC框架将request请求匹配到具体controller中的具体处理方法(method)这一角度切入,从源码入手,分析其具体实现原理与过程,同时也会穿插比较SpringMVC 5.x 和 SpringMVC 4.x 在流程实现过程中的差异。(文中所有代码均为SpringMVC 5.0.7 版本,由于篇幅长短原因,4.x版本的源码就不贴出来了,只会用图片和文字详细说明,大家也可自行参考其他博文)
~ . ~

请求匹配需要做些什么

大家不要被复杂的映射匹配逻辑给吓着了,其实以我个人理解,它整体上可以概括为2件事情,一是注册映射、二是匹配映射:

  1. 注册映射:扫描bean -> 检测注解信息 -> 创建映射 -> 保存映射至相关数据结构
  2. 匹配映射:根据url查找映射 -> 选取最符合的映射 -> 从中获取HandlerMethod
    注:这里match映射指的是一个Map,key为RequestMappingInfo,value为HandelMethod

~ . ~

一些重要的类

在具体分析之前,我们先看看具体过程中涉及的几个比较重要的类,理解他们很重要:

HandelMethod类:
是一个封装了方法参数、方法注解,方法返回值等众多元素的类,对请求的处理逻辑即在对应的方法里完成。其中方法参数的类型是一个MethodParameter类型的数组。

RequestCondition类:
一个接口类,是SpringMVC的映射基础中的请求条件,是映射匹配的关键接口。可以进行:
combine操作(与其他condition结合,如handler的类上和方法上均有注解,则会进行combine操作)
compareTo操作(通过HttpServletRequest与一个请求匹配,并与其它匹配相比较)
getMatchingCondition操作(获取合适的映射)。

RequestMappingInfo类:
封装了各种请求映射条件并实现了RequestCondition接口的类。里面有各种RequestCondition实现类属性,如http请求的路径模式、方法、参数、头部等信息。正是这些信息,才能准确无误地去定位一个HandlerMethod(用于具体处理请求的方法)。

~ . ~

具体分析

注册过程1:获取所有的bean

在AbstractHandlerMethodMapping类的initHandlerMethods方法中,会先找出spring容器初始化的所有bean。找出后,会遍历每一个bean,用isHandler方法判断每一个bean是否有@controller或@RequestMapping注解。如果有那么对当前的bean调用detectHandlerMethods方法进行下一步解析处理。遍历每一个bean的源码如下:

// 遍历每一个bean
for (String beanName : beanNames) {
    if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
        Class<?> beanType = null;
        try {
        // 首先会获取当前bean的类型
            beanType = obtainApplicationContext().getType(beanName);
        }
        catch (Throwable ex) {
            // 如果获取bean类型失败,说明此bean可能通过懒加载方式生成,直接忽略
            if (logger.isDebugEnabled()) {
                logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
            }
        }
        /**
         * 如果bean类型获取到了,再接着判断它是否为Handler
         * 即是否含有@controller@RequestMapping注解,
         * 若有,美滋滋,进一步检测,若没有直接搞下一个bean
         **/
        if (beanType != null && isHandler(beanType)) {
            detectHandlerMethods(beanName);
        }
    }
}

~ . ~

注册过程2:检测HandlerMethod并创建对应映射信息

4.x 5.x版本的共同点:

  1. detectHandlerMethods中会调用getMappingForMethod方法用来寻找用@controller或@RequestMapping注解标记的类或方法,找到后就调用createRequestMappingInfo方法来构造RequestMappingInfo,如果类和方法的注解都存在,那么创建好RequestMappingInfo将会进行combine操作。最终会返回创建好的RequestMappingInfo。
  2. 都会维护一个Map,其中key为Method(反射属性),value为RequestMappingInfo。用于后续注册。
    (SpringMVC 4.x 中名为mappings,SpringMVC 5.x 中名为methods)

(SpringMVC 4.x 中) 在detectHandlerMethods对注解进行解析的时候,会以MethodFilter匿名内部类方式筛选出RequestMappingInfo不为空的Method(通过子类RequestMappingHandlerMapping实现的getMappingForMethod获取RequestMappingInfo),将该method与RequestMappingInfo一同加入mappings以供后续注册。下图为SpringMVC 4.x 的检测HandlerMethod流程(图片来自于网络)
SpringMVC 5.0 请求映射匹配核心源码解读 (SpringMVC 5.x 与 SpringMVC 4.x 对比)
(SpringMVC 5.x 中): 因为上一步操作会筛选出被@controller或@RequestMapping注解标注的类(即handler)或方法,一定会有对应的注解(含有各类请求信息RequestMappingInfo),故在SpringMVC 4.0 对于method是否有对应的RequestMappingInfo判断是冗余的,而且其实在SpringMVC 4.x 筛选得到的Method Set中的method与最终的mappings Map中的method明显也冗余了。所以在SpringMVC 5.x 中的筛选方法里删除了判断Method的RequestMappingInfo是否为空的判断逻辑,如果在handler里没能找到对应的注解则会直接抛出异常。同时在5.x版本中直接用名为methods的Map保存筛选结果,避免了method的冗余。实现源码如下:

// AbstractHandlerMethodMapping.java 类中
/**
 * 在一个Handler(即含有特定注解的bean)中寻找HandlerMethod(可能有多个)
 * @param handler bean的名字或者是一个handler实例
 **/
protected void detectHandlerMethods(final Object handler) {
    Class<?> handlerType = (handler instanceof String ?
            obtainApplicationContext().getType((String) handler) : handler.getClass());

    if (handlerType != null) {
        final Class<?> userType = ClassUtils.getUserClass(handlerType);
        // 语法更新至JDK8(lambda表达式)
        // 筛选直接获得最终形式的Map(key为method,value为RequestMappingInfo)
        Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                (MethodIntrospector.MetadataLookup<T>) method -> {
                    try {
                        /**
                         * 调用getMappingForMethod方法用来寻找用
                         * @RequestMapping注解标记的类或方法,这里为一抽象
                         * 方法,具体实现在子类RequestMappingHandlerMapping里实现
                         **/
                        return getMappingForMethod(method, userType);
                    }
                    // 在handler里没能找到对应的注解则会直接抛出异常
                    // 即getMappingForMethod方法报错
                    catch (Throwable ex) {
                        throw new IllegalStateException("Invalid mapping on handler class [" +
                                userType.getName() + "]: " + method, ex);
                    }
                });
        if (logger.isDebugEnabled()) {
            logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
        }
        // 语法更新至JDK8(ForEach语法)
        methods.forEach((method, mapping) -> {
            Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
            registerHandlerMethod(handler, invocableMethod, mapping);
        });
    }
}

可以发现SpringMVC 5.x 中处理逻辑更加合理,而且语法也都用了JDK8新特性,如筛选方法中用lambda表达式方式代替匿名内部类方式。用ForEach语法使得Map里的结构清晰可见。

~ . ~

注册过程3:注册method

最后一步即将前两部得到的合法的方法映射信息向Spring注册,以便后续查找。其实我认为此过程主要做两部分工作:

  1. 根据method判断待注册的HandelMethod是否已经存在。
  2. 保存注册信息(RequestMappingInfo与HandelMethod的映射关系)

4.x 5.x版本的共同点:
根据method反射属性,创建一HandlerMethod对象,并从之前映射信息Map中根据RequestMappingInfo获取旧的HandlerMethod进行比对,若相同则抛出异常,因为一个请求映射信息只能对应一个方法,之前注册过的就不能再第二次注册了。若二者不相同,则进行下一步注册操作。

(SpringMVC 4.x 中): 这个大家应该比较熟悉了,将创建的新的HandelMethod加入到名为handlerMethods这个Map里(key为RequestMappingInfo,value为HandelMethod),handlerMethods保存了已有的RequestMappingInfo到HandlerMethod的映射。最后从RequestMappingInfo类型的mapping中获取MappingPathPatterns(本质为String)作为key,mapping作为value,添加到名为urlMap的MultiValueMap结构里。SpringMVC 4.x 注册源码如下:(图片来自于网络)
SpringMVC 5.0 请求映射匹配核心源码解读 (SpringMVC 5.x 与 SpringMVC 4.x 对比)

(SpringMVC 5.x 中): SpringMVC 5.x 中不再用以registerHandlerMethod维护urlMap的方式来注册method了,而是通过一个名为MappingRegistry的内部类集中管理注册信息。这个内部类里包含了:

  1. 记录已有RequestMappingInfo到HandlerMethod的映射的名为mappingLookup的Map(对应于 4.x 版本中的名为handlerMethods的Map)
  2. 记录请求pattern与RequestMappingInfo映射的名为urlLookup的MultiValueMap(相对于4.x版本的urlMap)
  3. 读写锁。
  4. 注册方法register
  5. 等等其他属性,详如下代码

可以看出,这样使得注册逻辑变得更为清晰。源码分析如下:

// AbstractHandlerMethodMapping.java 类中的MappingRegistry内部类
/**
* 一个MappingRegistry对象维护了所有的HandlerMethod映射
**/
class MappingRegistry{

  // 注册信息
  private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

  /**
   * 相当于4.x版本里的handlerMethods,用于记录已被映射的HandlerMethod
   * 根据method反射属性创建好HandlerMethod后,就是从这里面
   * 根据RequestMappingInfo找旧的HandlerMethod进行比对的
   **/
  private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

  // 相当于4.x版本中的urlMap,是一个MultiValueMap
  private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
  private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
  private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
  // 5.x 版本新加入的读写锁,注册时候会加上写锁
  private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

  // ... 省略getter、setter方法

  // 实际注册方法
  public void register(T mapping, Object handler, Method method) {
    // springMVC 5.x 中会对注册过程加入写锁
    this.readWriteLock.writeLock().lock();
    try {
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        /** 
         * 将新创建出的handlerMethod与原有的对比,若相同
         * 即表示原来的RequestMappingInfo已经注册到对应的
         * 方法中了,直接抛出异常。
         * 注:4.x版本是直接把比较逻辑暴露出来了,可以看到5.x版本
         * 在这些细节逻辑优化上做的很精致
         **/
        assertUniqueMethodMapping(handlerMethod, mapping);

        if (logger.isInfoEnabled()) {
            logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
        }
        /**
         * 将创建的新的handlerMethod作为value,RequestMappingInfo
         * 作为key加入到名为mappingLookup的Map中。(前面assertUniqueMethodMapping
         * 就是在这个map里找旧的handlerMethod进行比对的)
         **/
        this.mappingLookup.put(mapping, handlerMethod);

        List<String> directUrls = getDirectUrls(mapping);
        for (String url : directUrls) {
            /**
             * 将url与RequestMappingInfo建立映射,后续根据request
             * 的URL查RequestMappingInfo列表,从而进行后续匹配排序的
             * 时候就是从这里面查。(相当于4.x版本中望urlMap中添加数据)
             **/
            this.urlLookup.add(url, mapping);
        }

        String name = null;
        if (getNamingStrategy() != null) {
            name = getNamingStrategy().getName(handlerMethod, mapping);
            addMappingName(name, handlerMethod);
        }

        CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
        if (corsConfig != null) {
            this.corsLookup.put(handlerMethod, corsConfig);
        }
        /**
         * springMVC 5.x 中不再维护urlMap了,而是维护一个
         * 含有注册信息的MappingRegistry内部类。
         * 里面有各种注册信息,这个registry是记录注册信息的。
         **/
        this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
     }
     finally {
        this.readWriteLock.writeLock().unlock();
     }
}

至此,注册工作全部完成,下面分析根据request请求匹配到已注册好的HandlerMethod。

~ . ~

请求匹配映射查找过程:根据请求URL查找对应的HandlerMethod

注册事情万事俱备后,接下来就是对request请求进行匹配了。
过程大致是这样:在MVC架构的C层,即Controller层里,在Spring启动的时候,已经根据代码里的注解信息将每一个方法注册。之后对request进行方法映射匹配的时候便会直接从注册时保存的相关信息中去查找对应的请求处理方法,具体分析如下。

4.x 5.x 版本的共同点:
都是通过lookupHandlerMethod方法返回HandlerMethod。首先以lookupPath参数查询出一个RequestMappingInfo列表(因为一个请求URL可能匹配到多个符合条件的路径匹配规则)

(SpringMVC 4.x 中):
根据得到的RequestMappingInfo列表在HandlerMethods集合中找到HandlerMethod(1个或多个),根据RequestMappingInfo和HandlerMethod构造Match(1个或多个),之后根据匹配规则选出最匹配的Match,最后返回Match中的HandlerMethod属性。此HandlerMethod即最终处理request请求。如下:(图片来自网络)
SpringMVC 5.0 请求映射匹配核心源码解读 (SpringMVC 5.x 与 SpringMVC 4.x 对比)

(SpringMVC 5.x 中):
大体逻辑和 4.x 版本一致,也是通过请求url找到匹配的RequestMappingInfo列表(1个或多个),之后结合HandlerMethod构造Match(1个或多个)。不同的是 5.x 版本里,会将构造Match的过程进一步封装至用于注册的内部类MappingRegistry里(这样的改动确实挺合理,因为Match本来也就属于注册信息管理的范畴)。

(补充说明)最佳匹配选取规则:
请求路径pattern > 请求参数params > 请求头部header > Content-Type头部consumers > Accept头部 > 请求方法methods > 自定义方式custom。

// AbstractHandlerMethodMapping.java 类中
/**
 * 为当前request请求查找最匹配的HandlerMethod,如果根据lookupPath匹配了多个
 * RequestMappingInfo,那么根据匹配规则选择最符合的那个。
 * @param 当前servlet映射中的查找路径(可以理解为URL中的项目名之后的部分)
 * @param 当前request请求
 * @return 返回最匹配的HandlerMethod
 */
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<>();
    /**
     * springMVC 5.x 中不再从名为urlMap的MultiValueMap中获取
     * RequestMappingInfo列表,进而结合HandlerMethod构建match。
     * 而是从mappingRegistry里直接获取match列表(即将构建match的过程
     * 又进一步封装进了用于注册的内部类MappingRegistry里了,逻辑更加清晰)
    **/
    List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
    if (directPathMatches != null) {
        // 若有匹配到的,直接就放在matches列表里了
        addMatchingMappings(directPathMatches, matches, request);
    }
    if (matches.isEmpty()) {
        /**
         * 找不到,没办法了,只能遍历所有mapping了
         * 即利用MappingRegistry内部类中的mappingLookup对象(Map类型)
         * 中的keySet(即为内部元素为RequestMappingInfo的集合),来构建match
         **/
        addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
    }

    if (!matches.isEmpty()) {
        Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
        // 根据规格对matchs排序
        matches.sort(comparator);
        if (logger.isTraceEnabled()) {
            logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
        }
        // 选取最佳匹配项
        Match bestMatch = matches.get(0);
        if (matches.size() > 1) {
            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();
                throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
                        request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
            }
        }
        handleMatch(bestMatch.mapping, lookupPath, request);
        // 将最匹配的handlerMethod返回
        return bestMatch.handlerMethod;
    }
    else {
        // 没有匹配成功的
        return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
    }
}

至此返回的handlerMethod即为最终用于处理request请求的那个方法。
~.~

总结

1、SpringMVC 5.x 引入了JDK8语法新特性,目前看到的所有源码都改用了新特性。

2、逻辑封装更加细化清晰,代码更加精致。如新增MappingRegistry内部类去管理映射注册信息,使映射的建立和后续匹配变得更加简单。

3、常用设计模式要熟悉,阅读框架源码前最好熟悉常用的设计模式,工厂方法、适配器模式、代理模式等等,源码会教你如何灵活应用这些设计模式。如SpringMVC 5.x 版本在注册过程2:检测HandlerMethod并创建对应映射信息的过程中,调用的是createRequestMappingInfo代理方法:

// 代理方法
createRequestMappingInfo(AnnotatedElement element)

因为我们直观上创建一个RequestMappingInfo只需要面向对应的注解即可,但实际上仍会调用:

createRequestMappingInfo(RequestMapping, RequestCondition)

来完成真实的创建操作。因为创建操作过程会涉及对注解的解析、根据RequestCondition进行combine等操作。但是我们上层创建操作无需关注这些。故用代理模式。这些都是在SpringMVC 5.x 版本加入的。

4、第一遍看不懂,多看几遍就懂了,理解编码思想更重要。

-
-

SpringMVC 4.x 部分参考博文:
http://www.cnblogs.com/fangjian0423/p/springMVC-request-mapping.html#init