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注解的源码解析
文章目录
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 初始化源码—@RequestMapping注解的源码解析