Spring MVC 初始化源码—@RequestMapping注解的源码解析

Posted L-Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC 初始化源码—@RequestMapping注解的源码解析相关的知识,希望对你有一定的参考价值。

  基于最新Spring 5.x,详细介绍了Spring MVC中的@RequestMapping注解解析的源码。

  我正在参与CSDN《新程序员》有奖征文,活动地址:https://marketing.csdn.net/p/52c37904f6e1b69dc392234fff425442

  采用@RequestMapping注解以及使用@RequestMapping作为元注解的注解修饰方法来实现的Controller控制器将被解析为HandlerMethod类型的Handler处理器对象,该对象保存着URI路径到对应控制器方法的映射关系,后面请求的时候会根据路径找到Handler,然后拿到Handler对应的控制器方法直接执行,而无需再次解析。

  下面让我一起来看看基于@RequestMapping注解的Controller控制器的解析流程源码。下面的源码版本基于5.2.8.RELEASE

Spring MVC源码 系列文章

Spring MVC 初始化源码(1)—ContextLoaderListener与父上下文容器的初始化

Spring MVC 初始化源码(2)—DispatcherServlet与子容器的初始化以及MVC组件的初始化【一万字】

Spring MVC 初始化源码(3)—<mvc:annotation-driven >配置标签的源码解析

Spring MVC 初始化源码(4)—@RequestMapping注解的源码解析

Spring MVC 请求执行流程的源码深度解析【两万字】

1 @RequestMapping注解解析入口

  通常Handler处理器在容器启动的时候就被解析了,因此HandlerMethod也在启动时解析的,RequestMappingHandlerMapping实现了InitializingBean接口,在RequestMappingHandlerMapping被实例化之后的将会触发它的InitializingBean#afterPropertiesSet方法回调,在该方法中就会进行基于注解的控制器方法的解析。
  下面来看看基于@RequestMapping注解的方法控制器的解析原理。

/**
 * RequestMappingHandlerMapping的属性
 * <p>
 * 用于请求映射目的的配置选项的容器。
 * 这种配置是创建RequestMappingInfo实例所必需的,通常在所有RequestMappingInfo实例创建中使用。
 */
private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();


/**
 * RequestMappingHandlerMapping的方法
 */
@Override
@SuppressWarnings("deprecation")
public void afterPropertiesSet() {
    //将一系列属性设置到RequestMappingInfo.BuilderConfiguration中
    this.config = new RequestMappingInfo.BuilderConfiguration();
    this.config.setUrlPathHelper(getUrlPathHelper());
    this.config.setPathMatcher(getPathMatcher());
    this.config.setSuffixPatternMatch(useSuffixPatternMatch());
    this.config.setTrailingSlashMatch(useTrailingSlashMatch());
    this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
    this.config.setContentNegotiationManager(getContentNegotiationManager());
    /*调用父类AbstractHandlerMethodMapping的方法*/
    super.afterPropertiesSet();
}

/**
 * AbstractHandlerMethodMapping的方法
 * <p>
 * 在初始化时立即检测处理器方法。
 */
@Override
public void afterPropertiesSet() {
    //调用initHandlerMethods方法
    initHandlerMethods();
}

  initHandlerMethods方法用于在ApplicationContext中扫描bean,检测并注册Handler方法。

/**
 * 使用了scoped-proxy作用域代理的Bean名称前缀,前缀之后就是原始beanName
 */
private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";

/**
 * AbstractHandlerMethodMapping的方法
 * <p>
 * 在ApplicationContext中扫描bean,检测并注册Handler方法。
 */
protected void initHandlerMethods() {
    //getCandidateBeanNames()实际上会获取当前Spring MVC容器中所有bean的beanName,默认不会获取父Spring容器中的beanName
    //因此建议对不同类型的bean定义在不同的容器中,这样这里的循环遍历判断的时间就会有效减少
    for (String beanName : getCandidateBeanNames()) {
        //如果beanName不是以"scopedTarget."开头的,即不是作用域代理bean,那么处理此bean
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            //处理候选bean.确定指定的候选bean的类型,如果是handler类型
            //那么调用detectHandlerMethods方法在指定的 handler bean中查找handler方法
            //并注册一系列缓存到当前AbstractHandlerMethodMapping实例的mappingRegistry属性中(该属性对象内部的数据集合中)。
            processCandidateBean(beanName);
        }
    }
    //在检测到所有handler method之后调用,方法参数是mappingLookup缓存
    //目前版本中该方法主要目的仅仅是尝试打印日志信息,比如handlerMethods的总数
    handlerMethodsInitialized(getHandlerMethods());
}

  可以看到,initHandlerMethods方法将会获取当前Spring MVC容器中所有bean的beanName作为候选bean,默认不会获取父Spring容器中的beanName。
  随后对每一个候选bean循环调用processCandidateBean方法,该方法中将确定指定的候选bean的类型,如果是handler bean,那么调用detectHandlerMethods方法在指定的 handler bean中查找handler方法,并注册一系列缓存到当前AbstractHandlerMethodMapping实例的mappingRegistry属性中(该属性对象内部的缓存集合中)。

2 getCandidateBeanNames获取候选beanName

  getCandidateBeanNames()实际上会获取当前Spring MVC容器中所有bean的beanName,默认不会获取父Spring容器中的beanName。因此建议对不同类型的bean定义在不同的容器中,这样这里的循环遍历判断的时间就会有效减少
  这个标志很重要,默认情况下,父容器中的Handler方法不会被检测,仅会检测DispatcherServlet关联的子容器中的Handler方法。因此,一般情况下,如果Controller被存放在父容器中,则该Controller是失效的。这里的原理源码。

/**
 * AbstractHandlerMethodMapping的属性
 * <p>
 * 是否在祖先ApplicationContexts中的Bean中检测处理器方法。
 * 默认值为“ false”:仅考虑当前ApplicationContext中的bean,即仅在定义了此HandlerMapping本身的上下文中(通常是当前DispatcherServlet的上下文)。
 * 将此标志也打开,以检测祖先上下文(通常是Spring Root WebApplicationContext)中的处理程序bean。
 * <p>
 * 这个标志很重要,默认情况下,父容器中的Handler方法不会被检测,仅会检测DispatcherServlet关联的子容器中的Handler方法
 * 因此,一般情况下,如果Controller被存放在父容器中,则该Controller是失效的。
 */
private boolean detectHandlerMethodsInAncestorContexts = false;

/**
 * AbstractHandlerMethodMapping的方法
 * <p>
 * 在应用程序上下文中确定候选bean的名称。
 *
 * @return 候选bean的名称数组,默认是容器中的全部beanName
 */
protected String[] getCandidateBeanNames() {
    //是否在祖先ApplicationContexts中的Bean中检测处理器方法,默认false,将仅在当前DispatcherServlet的上下文检测
    //这里是尝试获取Object类型的beanName数组,由于类型是Object因此将会获取所有的注册的beanName
    return (this.detectHandlerMethodsInAncestorContexts ?
            BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
            obtainApplicationContext().getBeanNamesForType(Object.class));
}

3 processCandidateBean处理候选bean

  确定指定的候选bean的类型,如果是handler类型,那么调用detectHandlerMethods方法在在指定的 handler bean中查找handler方法,并注册一系列缓存到当前AbstractHandlerMethodMapping实例的mappingRegistry属性中(该属性对象内部的数据集合中)。

/**
 * AbstractHandlerMethodMapping的方法
 * <p>
 * 确定指定的候选bean的类型,如果是handler类型,那么调用detectHandlerMethods方法
 * <p>
 * 此实现通过检查org.springframework.beans.factory.BeanFactory#getType
 * 并使用bean名称调用detectHandlerMethods方法来避免bean创建。
 *
 * @param beanName 候选bean的名称
 */
protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;
    try {
        //根据beanName获取bean类型
        beanType = obtainApplicationContext().getType(beanName);
    } catch (Throwable ex) {
        // An unresolvable bean type, probably from a lazy bean - let's ignore it.
        if (logger.isTraceEnabled()) {
            logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
        }
    }
    //如果isHandler方法返回true,即当前类属于handler类
    if (beanType != null && isHandler(beanType)) {
        //那么在指定的handler bean中查找handler方法
        //并注册到当前AbstractHandlerMethodMapping实例的mappingRegistry属性中
        detectHandlerMethods(beanName);
    }
}

3.1 isHandler是否是handler类

  该方法判断给定类型是否是一个handler类,期望处理器类具有类级别的@Controller注解或类型级别的@RequestMapping注解,也就是说bean的类上如果具有这两个注解之一,那么该类就是handler类型。

/**
 * RequestMappingHandlerMapping的方法
 * <p>
 * 给定类型是否是一个handler类
 * <p>
 * 期望处理器类具有类级别的@Controller注解或类型级别的@RequestMapping注解
 * 如果具有这两个注解之一,那么该类就是handler类型
 *
 * @param beanType 被检查的bean的类型
 * @return 如果是handler类型,则为“ true”,否则为“ false”。
 */
@Override
protected boolean isHandler(Class<?> beanType) {
    //类上是否具有@Controller或者@RequestMapping注解
    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
            AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

3.2 detectHandlerMethods解析HandlerMethod

  在指定的handler bean中查找handler方法,并注册到当前AbstractHandlerMethodMapping实例的mappingRegistry属性中。

/**
 * AbstractHandlerMethodMapping的方法
 * <p>
 * 在指定的handler bean中查找handler方法并注册到缓存中
 *
 * @param handler Bean名称或实际handler实例
 */
protected void detectHandlerMethods(Object handler) {
    //获取handler的类型
    Class<?> handlerType = (handler instanceof String ?
            obtainApplicationContext().getType((String) handler) : handler.getClass());

    if (handlerType != null) {
        Class<?> userType = ClassUtils.getUserClass(handlerType);
        //查找该类的方法,并解析方法和类上的@RequestMapping注解创建RequestMappingInfo对象,T为RequestMappingInfo类型的实例
        //最终返回一个map,每个方法都有对应的RequestMappingInfo实例,如果该方法上没有@RequestMapping注解,则value为null
        Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                (MethodIntrospector.MetadataLookup<T>) method -> {
                    try {
                        /*
                         * 1 查找该方法对应的RequestMappingInfo映射
                         *
                         * 该方法由子类实现,对于RequestMappingHandlerMapping,实际上就是
                         * 解析方法上的@RequestMapping注解并创建一个RequestMappingInfo对象
                         * 即将@RequestMapping注解的属性设置给RequestMappingInfo对应的属性,如果没有该注解,则返回null
                         */
                        return getMappingForMethod(method, userType);
                    } catch (Throwable ex) {
                        throw new IllegalStateException("Invalid mapping on handler class [" +
                                userType.getName() + "]: " + method, ex);
                    }
                });
        if (logger.isTraceEnabled()) {
            logger.trace(formatMappings(userType, methods));
        }
        //遍历获取到的map
        methods.forEach((method, mapping) -> {
            //根据对象的类型(可能是代理对象类型),解析当前方法成为一个真正可执行方法
            //为什么要这么做?因为实际执行该方法的对象可能是一个代理对象,进而在调用该方法时抛出各种异常
            //这个方法我们在Spring事件发布机制的源码文章中就详细讲解过了,在此不再赘述
            Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
            /*
             * 2 注册handlerMethod的一系列映射关系
             */
            registerHandlerMethod(handler, invocableMethod, mapping);
        });
    }
}

3.2.1 getMappingForMethod获取RequestMappingInfo映射

  解析方法和类级别的@RequestMapping注解(或者以该注解为元注解的注解),创建该handler方法的RequestMappingInfo映射。如果方法没有@RequestMapping注解,则获取null。
  该方法由子类实现,对于RequestMappingHandlerMapping,实际上就是解析方法上的@RequestMapping注解并创建一个RequestMappingInfo对象,即将@RequestMapping注解的属性设置给RequestMappingInfo对应的属性,如果没有该注解,则返回null。
  如果在类和方法上都存在@RequestMapping注解。那么最终的RequestMappingInfo将会并两个注解的属性,比如对于该handler的访问URI,一般就是类上的注解的path路径位于方法上的注解的path路径之前。

/**
 * RequestMappingHandlerMapping的方法
 * <p>
 * 解析方法和类级别的@RequestMapping注解(或者以该注解为元注解的注解)创建该handler方法的RequestMappingInfo映射。
 *
 * @param handlerType handler类的CLass
 * @param method      handler类的某个Method
 * @return 创建的RequestMappingInfo;如果方法没有@RequestMapping注解,则为null。
 */
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
    //解析方法上的@RequestMapping注解并创建一个RequestMappingInfo对象
    //即将@RequestMapping注解的属性设置给RequestMappingInfo对应的属性,如果没有该注解,则返回null
    RequestMappingInfo info = createRequestMappingInfo(method);
    if (info != null) {
        //解析类上的@RequestMapping注解并创建一个RequestMappingInfo对象
        //即将@RequestMapping注解的属性设置给RequestMappingInfo对应的属性,如果没有该注解,则返回null
        RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
        //如果类上存在@RequestMapping注解注解
        if (typeInfo != null) {
            //那么类上的注解信息和方法上的注解信息联合起来,也就是合并一些属性
            //对于path路径属性的联合,主要就是在方法的注解path前面加上类上的注解的path路径
            info = typeInfo.combine(info);
        }
        //获取该handler的额外访问URI路径前缀,一般为null
        //但我们可以手动配置RequestMappingHandlerMapping的pathPrefixes属性
        String prefix = getPathPrefix(handlerType);
        //如果存在对该handler的URI前缀
        if (prefix != null) {
            //则在path前面继续加上路径前缀,后续访问时可以加上前缀路径,也可以不加
            info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
        }
    }
    return info;
}

3.2.1.1 createRequestMappingInfo创建RequestMappingInfo

  该方法查找方法/类上的@RequestMapping注解并创建一个RequestMappingInfo构建者模式。该注解可以是直接声明的注解,元注解,也可以是在注解层次结构中合并注解属性的综合结果(即这个注解可以来自于父类)。

/**
 * RequestMappingHandlerMapping的方法
 * <p>
 * 委托createRequestMappingInfo(RequestMapping,RequestCondition)
 * 根据所提供的annotatedElement是类还是方法来提供适当的自定义RequestCondition。
 *
 * @param element 需要创建RequestMappingInfo的源数据,可能是方法或者类
 */
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
    //获取方法/类上的@RequestMapping注解,支持从父类的方法/类上查找
    RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
    //处理该请求的条件,这个版本无论是方法还是类都返回null
    RequestCondition<?> condition = (element instanceof Class ?
            getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
    //通过方法上的@RequestMapping注解以及RequestCondition创建一个RequestMappingInfo,
    return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}


/**
 * RequestMappingHandlerMapping的方法
 * <p>
 * 通过方法/类上的@RequestMapping注解创建一个RequestMappingInfo,构建者模式。
 * 该注解可以是直接声明的注解,元注解,也可以是在注解层次结构中合并注解属性的综合结果。
 *
 * @param customCondition 自定义的条件
 * @param requestMapping  注解
 */
protected RequestMappingInfo createRequestMappingInfo(
        RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
    RequestMappingInfo.Builder builder = RequestMappingInfo
            //解析注解的value和path属性,即请求的URI路径,这是一个数组
            //path路径支持${..:..}占位符,并且支持普通方式从外部配置文件中加载进来的属性以及environment的属性。
            //path路径还支持SPEL表达式(首先解析占位符,然后解析SPEL表达式)
            .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
            //解析注解的method属性,即请求方法,这是一个数组
            .methods(requestMapping.method())
            //解析注解的params属性,即请求参数匹配,这是一个数组
            .params(requestMapping.params())
            //解析注解的headers属性,即请求头匹配,这是一个数组
            .headers(requestMapping.headers())
            //解析注解的consumes属性,即请求的Content-Type匹配,这是一个数组
            .consumes(requestMapping.consumes())
            //解析注解的produces属性,即请求的Accept匹配,这是一个数组
            .produces(requestMapping.produces())
            //解析注解的name属性,即映射名称
            .mappingName(requestMapping.name());
    if (customCondition != null) {
        builder.customCondition(customCondition);
    }
    //构建RequestMappingInfo
    return builder.options(this.config).build();
}

3.2.1.2 combine合并属性

  如果方法和类上都有@RequestMapping注解,那么对于俩个注解的属性执行属性合并操作,最终返回一个新的RequestMappingInfo对象。

/**
 * RequestMappingInfo的方法
 * <p>
 * 将“此”请求映射信息(即当前实例)与另一个请求映射信息实例结合起来。
 *
 * @param other 另一个请求映射信息实例.(即方法上的注解解析的RequestMappingInfo)
 * @return 一个新的请求映射信息实例,永不为null
 */
@Override
public RequestMappingInfo combine(RequestMappingInfo other) {
    //name属性,只有一个注解具有该属性,那么就是使用该注解设置的属性
    //如果两个注解都具有该属性,那么使用"#"联合这两个属性,类注解属性在前
    String name = combineNames(other);
    //合并PatternsRequestCondition,最主要就是合并两个注解的path属性,即URI路径
    //原理比较复杂,将会对两个注解的path数组对应的索引的值进行合并
    //如果只有一个注解具有该属性,那么就是使用该注解设置的属性
    //如果两个注解都具有该属性,那么要分情况讨论,比较复杂,比如路径通配符,后缀通配符等
    //最常见的情况就是在方法的注解path前面加上类上的注解的path路径,它们之间使用"/"分隔
    PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
    //合并methods、Params、headers属性,如果只有一个注解具有该属性,那么就是使用该注解设置的属性
    //否则最终合并的结果将是两个注解的属性直接合并之后的结果(去重)
    RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
    ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
    HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
    //合并consumes、produces属性,如果参数注解(方法上的注解)具有该属性,那么就使用参数注解的属性,否则使用当前注解的属性(类注解的属性)
    ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
    ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
    //合并customCondition属性,一般都为null
    RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);

    //根据合并的属性创建一个新的RequestMappingInfo
    return new RequestMappingInfo(name, patterns,
            methods, params, headers, consumes, produces, custom.getCondition());
}

3.2.1.3 pathPrefixes路径前缀

  自Spring5.1开始,支持为某个handler类下面的所有handler方法配置URI路径前缀,在请求的时候就可以在path之前加上访加上前缀路径,当前也可以不加。
  前缀路径通过RequestMappingHandlerMapping的pathPrefixes属性来配置,该属性是一个LinkedHashMap。key为前缀路径字符串,value为对应的Predicate断言,如果某个handler的Class满足Predicate,那么就可以使用key作为路经前缀。
  在断言的时候,如果因此如果有多个能够匹配的Predicate,由于是一次匹配,并且集合是LinkedHashMap,那么配置在前面的路径前缀将被使用。

//RequestMappingHandlerMapping的属性

/**
 * SPring 5.1新增的路径前缀属性map
 * <p>
 * key为前缀路径字符串,value为对应的Predicate断言,如果某个handler的Class满足Predicate,那么就可以使用key作为路经前缀
 */
private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>();

/**
 * 用于执行${..:..}占位符匹配和SPEL表达式解析
 */
@Nullable
private StringValueResolver embeddedValueResolver;

/**
 * RequestMappingHandlerMapping的方法
 * <p>
 * 获取当前handler类的路径前缀
 *
 * @param handlerType 当前handler类的Class
 * @return 前缀路径,没有则返回null
 */
@Nullable
String getPathPrefix(Class<?> handlerType) {
    //遍历map依次匹配,因此如果有多个能够匹配的Predicate,那么配置在前面的最先匹配
    for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
        //执行Predicate的test方法尝试断言,如果可以匹配该Class,那么获取key作为路径前缀
        if (entry.getValue().test(handlerType)) {
            String prefix = entry.getKey();
            //前缀路径字符串还支持${..:..}占位符匹配和SPEL表达式
            if (this.embeddedValueResolver != null) {
                prefix = this.embeddedValueResolver.resolveStringValue(prefix);
            }
            return prefix;
        }
    }
    return null;
}

3.2.2 registerHandlerMethod注册HandlerMethod

  注册handler方法及其唯一映射的一系列对应关系,在项目启动时会为每个检测到的handler方法调用该方法,后续请求到来的时候可以直接从注册表中获取并使用,无需再次解析。

/**
 * RequestMappingHandlerMapping的方法
 *
 * @param handler handler的beanName或者handler实例
 * @param method 注册方法
 * @param mapping 与handler方法关联的映射条件RequestMappingInfo
 */
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
    //调用父类AbstractHandlerMethodMapping的同名方法
    super.registerHandlerMethod(handler, method, mapping

以上是关于Spring MVC 初始化源码—@RequestMapping注解的源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Spring MVC源码 ----- 启动过程与组件初始化

Spring MVC组件源码分析

Spring MVC 初始化源码—@RequestMapping注解的源码解析

Spring MVC源码——Root WebApplicationContext

Spring6源码・MVC请求处理流程源码解析

Spring MVC 初始化源码—DispatcherServlet与子容器的初始化以及MVC组件的初始化一万字