从源码看RequestMappingHandlerMapping的注册与发现
1.问题的产生
日常开发中,大多数的API层中@Controller注解和@RequestMapping注解都会被使用在其中,但是为什么标注了@Controller和@RequestMapping注解之后,从外部访问你设置的请求路径,springmvc就能捕获到请求并将请求交给指定的类和方法来执行呢?这其实就是RequestMappingHandlerMapping的核心功能了,注册&发现。
2.RequestMappingHandlerMapping之注册
@Controller和@RequestMapping是如何生效的呢,@Controller与其他的@Service,@Component又有什么区别呢,那么就来看下RequestMappingHandlerMapping是如何工作的。
RequestMappingHandlerMapping会在Spring容器初始化时实例化Bean注入到Spring容器中,RequestMappingHandlerMapping继承于RequestMappingInfoHandlerMapping,
RequestMappingInfoHandlerMapping又继承自AbstractHandlerMethodMapping,AbstractHandlerMethodMapping又实现了InitializingBean接口,InitializingBean接口我们都知道,
在Bean的生命周期中,在实例化Bean对象后,开始注入bean依赖的属性,如果实现了BeanNameAware接口,那么紧接着会执行该接口的setBeanName(String name)进行beanName的回调,
之后如果实现了InitializingBean接口则会调用afterPropertiesSet()方法进行一些其他属性的设置。
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
implements MatchableHandlerMapping, EmbeddedValueResolverAware {}
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
}
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {}
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
核心就在AbstractHandlerMethodMapping实现的afterPropertiesSet()中,可以看到afterPropertiesSet()中调用了initHandlerMethods()方法
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
protected void initHandlerMethods() {
//遍历spring容器中所有beanName
for (String beanName : getCandidateBeanNames()) {
//如果beanName不是以scopedTarget.开头则对该bean进行处理
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
//获取该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);
}
}
//如果类型!=null&&类型属于handler则进行检测
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
//RequestMappingHandlerMapping实现的isHandler方法
@Override
protected boolean isHandler(Class<?> beanType) {
//判断该类上是否有@Controller注解或者@RequestMapping注解
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
//检测handler的方法
protected void detectHandlerMethods(Object handler) {
//获取bean类型
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
//获取该类的用户自定义类,该ClassUtils是Spring提供的,通常情况下会返回该类本身的class,如果是cglib代理类则会去返回被代理类本身的class
Class<?> userType = ClassUtils.getUserClass(handlerType);
//将该类的每个方法和对应的url路径(类上@RequestMapping的信息 + 方法上的@RequestMapping的信息进行拼装)封装到map中
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
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,将所有方法和对应的mapping信息进行注册
methods.forEach((method, mapping) -> {
//通过AopUtils处理获取到反射可以调用的方法
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
//将该方法以HandlerMethod的形式进行注册,在SpringMvc的视角下,controller相当于handler,下属方法相当于handlerMethod
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
getMappingForMethod(),通过方法获取详细的Mapping信息(类+方法)
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//先获取方法上@RequestMapping的路径
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
//获取类上的@RequestMapping的路径
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
//如果类上存在@RequestMapping则将二者合并,所有路径由类的在前,后面拼接方法上的
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}
selectInvocableMethod()通过反射获取实际调用的方法
public static Method selectInvocableMethod(Method method, @Nullable Class<?> targetType) {
if (targetType == null) {
return method;
} else {
//通过反射获取到方法
Method methodToUse = MethodIntrospector.selectInvocableMethod(method, targetType);
//判断是否为私有方法/非静态方法/SpringProxy的子类,是则抛出异常,这里有个疑问,反射应该是不能调用静态方法,这里不知道为什么不限制静态方法?
if (Modifier.isPrivate(methodToUse.getModifiers()) && !Modifier.isStatic(methodToUse.getModifiers()) && SpringProxy.class.isAssignableFrom(targetType)) {
throw new IllegalStateException(String.format("Need to invoke method '%s' found on proxy for target class '%s' but cannot be delegated to target bean. Switch its visibility to package or protected.", method.getName(), method.getDeclaringClass().getSimpleName()));
} else {
return methodToUse;
}
}
}
将方法以handlerMethod的形式进行注册
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
} public void register(T mapping, Object handler, Method method) {
public void register(T mapping, Object handler, Method method) {
//开启写锁,防止多线程注册多个相同的handleMehtod
this.readWriteLock.writeLock().lock();
try {
//构造handlerMethod
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
//校验该handlerMehtod是否可以注册进来
validateMethodMapping(handlerMethod, mapping);
//获取该方法的直接路径
Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);
//将该方法的所有直接路径添加mappingRegister中的一个容器pathLookup,主要用来储存直接路径和mapping信息的关系
for (String path : directPaths) {
this.pathLookup.add(path, mapping);
}
String name = null;
if (getNamingStrategy() != null) {
//获取该方法的名称,如果@RequestMapping上的name有值则取该name,没有则根据类的类型名转换成大写的后+'#'+方法的名称 拼接而成
name = getNamingStrategy().getName(handlerMethod, mapping);
//将该handlerMehod添加进mappingRegister中的一个容器nameLookup,用来存存储name和handleMethod的关系
addMappingName(name, handlerMethod);
}
//跨越相关配置
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
corsConfig.validateAllowCredentials();
this.corsLookup.put(handlerMethod, corsConfig);
}
//将handleMehod注册进容器
this.registry.put(mapping,
new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}
validateMethodMapping校验handlerMethod是否合法
private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) {
// Assert that the supplied mapping is unique.
//根据mapping路径获取HandlerMethod
HandlerMethod existingHandlerMethod = this.mappingLookup.get(mapping);
//如果存在并且跟当前的HandlerMethod并且不是同一个则抛错
if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) {
throw new IllegalStateException(
"Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
handlerMethod + "\nto " + mapping + ": There is already '" +
existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped.");
}
}
3.RequestMappingHandlerMapping之发现
通过上面部分的源码我们大概知道了@Controller和@RequestMapping注解是如何生效的,RequestMappingHandlerMapping是如何去处理他们的,那我们说完了注册,就该来说发现,当客户端的请求发送到后台服务器时,SpringMvc是如何根据请求url给他找到指定的方法上呢?我们都知道SpringMVC处理请求的核心在于DisPatcherServlet,也就是所谓的前端控制器,DispatchServlet会拿到请求进行解析分发,那具体是如何操作的,来看下图:

图片来自百度,这张图大家应该都很熟悉了,DispatcherServlet的工作流程,可以看到DispatcherServlet在接收到客户端发送的request时会将请求委托给HandlerMapping去处理,HandlerMapping将根据url寻找一个合适的handler返回给DispatcherServlet,RequestMappingHandlerMapping实现自HandlerMapping接口,他是SpringMVC的众多HandlerMapping中的一个,它们都在Spring容器初始化时一同创建并注入到容器中。而我们的RequestMappingHandlerMapping的发现功能就在此时派上了用场。
当请求进来时,dispatcherServlet的doDispatch()方法将对请求处理,部分代码如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 可以看到在此时就要通过handlerMapping去获取HandlerExecutionChain执行链,其中包括了handler和对应的拦截器Inteceptor等等。
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
getHandler方法如下:
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//如果handlerMapping集合不为null,则遍历每个handlerMapping看是否可以找到对应的handler
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
这里我们主要来看RequestMappingHandlerMapping实现的getHandler()方法,查看源码发现RequestMappingHandlerMapping并没有重写该方法,而是由其祖宗类AbstractHandlerMapping重写了该方法
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//获取handler又是通过getHandlerInternal()方法
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
// Ensure presence of cached lookupPath for interceptors and others
if (!ServletRequestPathUtils.hasCachedPath(request)) {
initLookupPath(request);
}
//根据handler和request获取执行链
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (logger.isTraceEnabled()) {
logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
logger.debug("Mapped to " + executionChain.getHandler());
}
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = getCorsConfiguration(handler, request);
if (getCorsConfigurationSource() != null) {
CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
config = (globalConfig != null ? globalConfig.combine(config) : config);
}
if (config != null) {
config.validateAllowCredentials();
}
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
//返回执行链
return executionChain;
}
getHandlerInternal(request)方法又是由AbstractHandlerMapping的子类AbstractHandlerMethodMapping重写的,
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
//对mappingRegister加读锁,防止读取的时候被篡改数据
this.mappingRegistry.acquireReadLock();
try {
//根据request获取handlerMehtod,即找到了实际要执行的方法
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
//释放读锁
this.mappingRegistry.releaseReadLock();
}
}
4. 其他的一些补充
在阅读源码中我们也是在不断的在学习,其中的一些方法可能也是第一次见,这里也做解析让自己学习一下
ClassUtils.getUserClass(Class<?> clazz)方法我们在源码中看到有使用来根据calss获取用户自定义类,注意该ClassUtils为Spring包下的
public static Class<?> getUserClass(Class<?> clazz) {
//判断类名称是否包含cglib的分隔符 CGLIB_CLASS_SEPARATOR = "$$";
if (clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) {
//是则取获取当前cglib类的父类,因为cglib代理类是增强类,并不是被代理类本身
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
return superclass;
}
}
return clazz;
}

可以看到这里cglib代理类的的名称是拼接而成,其中是带着"$$"符号
5. 总结
关于RequestMappingHandlerMapping的源码解析暂时先到这了,之后可能会随时更改其中的内容,因为本人目前能力有限,文章中可能存在一些错误的地方,希望大家可以指出一起学习探讨。
从源码看RequestMappingHandlerMapping的注册与发现的更多相关文章
- 从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计
使用微信小程序开发已经很长时间了,对小程序开发已经相当熟练了:但是作为一名对技术有追求的前端开发,仅仅熟练掌握小程序的开发感觉还是不够的,我们应该更进一步的去理解其背后实现的原理以及对应的考量,这可能 ...
- 从微信小程序开发者工具源码看实现原理(四)- - 自适应布局
从前面从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计可以知道,小程序大部分是通过web技术进行渲染的,也就是最终通过浏览器的dom tree + cssom来生成渲染树:既然最终是通 ...
- .NET Core 3.0之深入源码理解Startup的注册及运行
原文:.NET Core 3.0之深入源码理解Startup的注册及运行 写在前面 开发.NET Core应用,直接映入眼帘的就是Startup类和Program类,它们是.NET Core应用程 ...
- 从linux源码看epoll
从linux源码看epoll 前言 在linux的高性能网络编程中,绕不开的就是epoll.和select.poll等系统调用相比,epoll在需要监视大量文件描述符并且其中只有少数活跃的时候,表现出 ...
- 从源码看Azkaban作业流下发过程
上一篇零散地罗列了看源码时记录的一些类的信息,这篇完整介绍一个作业流在Azkaban中的执行过程,希望可以帮助刚刚接手Azkaban相关工作的开发.测试. 一.Azkaban简介 Azkaban作为开 ...
- 解密随机数生成器(二)——从java源码看线性同余算法
Random Java中的Random类生成的是伪随机数,使用的是48-bit的种子,然后调用一个linear congruential formula线性同余方程(Donald Knuth的编程艺术 ...
- 从源码看Android中sqlite是怎么通过cursorwindow读DB的
更多内容在这里查看 https://ahangchen.gitbooks.io/windy-afternoon/content/ 执行query 执行SQLiteDatabase类中query系列函数 ...
- 从源码看Android中sqlite是怎么读DB的(转)
执行query 执行SQLiteDatabase类中query系列函数时,只会构造查询信息,不会执行查询. (query的源码追踪路径) 执行move(里面的fillwindow是真正打开文件句柄并分 ...
- 从Chrome源码看浏览器的事件机制
.aligncenter { clear: both; display: block; margin-left: auto; margin-right: auto } .crayon-line spa ...
随机推荐
- POJ 1442 Air Raid(DAG图的最小路径覆盖)
题意: 有一个城镇,它的所有街道都是单行(即有向)的,并且每条街道都是和两个路口相连.同时已知街道不会形成回路. 可以在任意一个路口放置一个伞兵,这个伞兵会顺着街道走,依次经过若干个路口. 问最少需要 ...
- 策略路由——使用Router-Policy策略路由进行路由协议的引入
1.实验目的:实现R3-R2-R1为访问主线路,R3-R4-R1为访问备份线路 2.实验拓扑及IP,如图; 3.基本配置(端口IP) R1: <Huawei>sys[Huawei]sys ...
- 记一次排查CPU高的问题
背景 将log4j.xml的日志级别从error调整为info后,进行压测发现CPU占用很高达到了90%多(之前也就是50%,60%的样子). 问题排查 排查思路: 看进程中的线程到底执行的是什么, ...
- 如何利用SimpleNVR建立全天候远程视频监控系统
随着社会经济的发展,5G.AI.云计算.大数据.物联网等新兴技术迭代更新的驱动下,传统的安防监控早已无法满足我们的需求.那么我们如何建立全天候远程视频监控系统来替代传统监控呢?如何进一步优化城市管理. ...
- 《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)
1.简介 在做web自动化时,有些情况selenium的api无法完成,需要通过第三方手段比如js来完成实现,比如去改变某些元素对象的属性或者进行一些特殊的操作,本文将来讲解怎样来调用JavaScri ...
- Unity——技能系统(二)
Unity技能系统(二) Unity技能系统(一) Demo展示: 五.技能管理和释放 1.CharacterSkillSystem 技能系统类,给外部(技能按钮,按键)提供技能释放方法: 技能释放逻 ...
- 菜鸡的Java笔记 - java 多对多映射
要求: 1.将数据还原为简单java类 2.数据的输出: 可以根据一个用户输出它对应的角色以及每个角色对应的权限,以及包含的具体的权限详情: 一个权限可以输出具 ...
- [hdu7042]二叉树
考虑最后这棵二叉树的结构,不难发现被移动的点在原树或新树中构成的都是若干棵完整的子树 (若$x$被移动,则$x$在原树或新树的子树中所有点都会被移动) 先在原树中考虑此问题,对于每一棵由被移动的点所构 ...
- C/C++ Qt ToolBar 菜单组件应用
ToolBar工具栏在所有窗体应用程序中都广泛被使用,使用ToolBar可以很好的规范菜单功能分类,用户可根据菜单栏来选择不同的功能,Qt中默认自带ToolBar组件,当我们以默认方式创建窗体时,To ...
- 安装maven配置maven环境变量
在官网下载maven的包 我们下载的是:apache-maven-3.5.2-bin.zip 3.解压缩maven的包到某个目录中 4.配置maven的环境变量 配置M2_HOME环境变量为maven ...