揭秘@Controller内部方法与URL绑定的全流程
在MVC
的开发模式中,在控制层
中,通常会用@RequestMappring
注解来定义一个资源链接
的请求url
,然后编写该url
所对应的特定处理逻辑。
进一步,为了@RequestMappring
标注的类能被Spring
容器管理,通常还会在相关类上使用@Controller
注解,但用了这么长时间的@Controller
,你是否了解过Spring
框架内部是如何维护url
与方法
之间绑定关系的? 不了解也没关系,今天我们便来扒一扒Spring
框架对@Controller
解析的全流程!
前言
对于一个Springboot
而言,当其收到一个Http
请求到后,在其内部会通过DispatcherServlet # doDispatch
来实现对请求相对分发、执行`。 其处理逻辑如下:
总结来看,这一过程完成大致完成如下逻辑:
- 分发,即根据请求寻找对应的执行方法。 这个过程主要涉及到
url
与处理器
之间的匹配。这一过程中会涉及到的组件有:HandlerMapping(处理器映射器)、HandlerAdapter(处理器适配器)
等。 - 执行,反射执行寻找到的执行方法。 这一过程的本质就是通过
反射
机制来调用被@ReqeustMapping
标注的方法
。
而在执行
能顺利执行的前提则在于@Controller
所标注的类能顺利注入容器
,并实现url
与处理方法
的绑定。接下来,我们便来看看Spring
内部对于@Controller
所标注的类究竟是通过何种方式来注入到Spring
容器内部。
@Controller
内部方法绑定全流
开始分析之前,我们先对SpringMVC
中的HandlerMapping
组件进行一个简单的回顾。在SpringMVC
中,HandlerMapping
(处理器映射)的主要用途在于将传入的HTTP
请求映射到相应的处理器方法上。
具体而言,在SpringMVC
中,DispatcherServlet
需要处理分发很多请求,而每个请求通常对应一个特定的Handler
来进行处理。而接收到一个请求后,具体使用哪个Handler
来处理则需要通过HandlerMaping
进行处理。换言之,对于一个SpringBoot
应用而言,其主要在HandlerMapping
组件中完成了url
与控制器
的匹配工作。
因此,如果我们想探究@Controller
的解析过程,HandlerMapping
不失为一个很好的切入点。所以我们不妨深入到HandlerMapping
的公共父类AbstractHandlerMethodMapping
来看看其内部是否有对@Controller
注解的解析。其中,AbstractHandlerMethodMapping
的类结构如下图所示
可以看到对于AbstractHandlerMethodMapping
而言,其实现了一个名为InitializingBean
的接口。而InitializingBean
接口,通常用于在bean
实例化之后,进行一些初始化工作。 如果一个类实现该接口,那么当其实例化完成后,Spring
容器自动调用afterPropertiesSet()
方法。从而为bean
提供一种在bean
被完全初始化后、但在其实际使用之前执行定制初始化逻辑的机会。
进一步,AbstractHandlerMethodMapping
内部
afterPropertiesSet()
的调用逻辑如下所示:
不难看出,afterPropertiesSet
的处理逻辑全部委托于initHandlerMethods
,而initHandlerMethods
方法的核心在于 getCandidateBeanNames
和processCandidateBean
。
protected void initHandlerMethods() {
// <1> 遍历 Bean ,逐个处理
for (String beanName : getCandidateBeanNames()) {
// 排除目标代理类,AOP 相关,可查看注释
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
// <2> 处理 Bean
processCandidateBean(beanName);
}
}
//<3> 空方法,暂无具体的实现,可用于后续扩展()
handlerMethodsInitialized(getHandlerMethods());
}
我们首先来看getCandidateBeanNames
内部的逻辑:
protected String[] getCandidateBeanNames() {
// 获取上下文中所有的 Bean 的名称
return (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class)
: obtainApplicationContext().getBeanNamesForType(Object.class));
}
可以看到,对于getCandidateBeanNames
方法而言, 其会根据detectHandlerMethodsInAncestorContexts
的值来选择不同的方式获取bean
信息。具体来看,如果detectHandlerMethodsInAncestorContexts
为true
则通过BeanFactoryUtils.beanNamesForTypeIncludingAncestors
来获取包括祖先上下文中的所有bean
的名称;而当其为false
时,则直接通过obtainApplicationContext().getBeanNamesForType(Object.class)
方法获取当前上下文中所有bean
的名称。返回值是一个字符串数组,包含了所有候选bean
的名称。
(Ps:在AbstractHandlerMethodMapping
内部,detectHandlerMethodsInAncestorContexts
变量默认置为false
,因此其默认只会获取当前容器的bean
信息,而不会主动加载父容器的bean
信息。)
总结来看,getCandidateBeanNames
会获取当前容器中所有的bean
的名称集合。然后,在逐个遍历交由processCandidateBean
处理。* 而processCandidateBean
逻辑如下:
protected void processCandidateBean(String beanName) {
// <1> 获得 Bean 对应的 Class 对象
Class<?> beanType = null;
// ... 省略相关异常捕获代码
beanType = obtainApplicationContext().getType(beanName);
// ... 省略相关异常捕获代码
// <2> 判断 Bean 是否有 @Controller 或者 @RequestMapping 注解
if (beanType != null && isHandler(beanType)) {
// <3> 扫描处理器方法
detectHandlerMethods(beanName);
}
}
对于processCandidateBean
而言,其大致逻辑又可以分为如下两点:
- 尝试获取指定
beanName
的bean
信息 - 调用
isHandler
方法对对获取到的bean
判断,如果可以处理则调用detectHandlerMethods
方法.
(Ps: isHandler
在子类 RequestMappingHandlerMapping
中进行了实现,其主要用于判断类内部是否有 @Controller
或者 @RequestMapping
注解.)
detectHandlerMethods
相关逻辑
protected void detectHandlerMethods(Object handler) {
// <1> 获得 Bean 对应的 Class 对象
Class<?> handlerType = (handler instanceof String
? obtainApplicationContext().getType((String) handler)
: handler.getClass());
if (handlerType != null) {
// <2> 获得真实的 Class信息
Class<?> userType = ClassUtils.getUserClass(handlerType);
// <3> 获得匹配的方法和对应的 Mapping 对象
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
// 创建该方法对应的 Mapping 对象,
return getMappingForMethod(method, userType);
});
// <4> 遍历方法,逐个注册 HandlerMethod
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
其中<3>
处的 getMappingForMethod
为一个抽象方法,其在RequestMappingHandlerMapping
中有具体实现。其在RequestMappingHandlerMapping
中会为标有@RequestMapping
注解的方法创建 RequestMappingInfo
对象,以而实现控制器类和方法上的 @RequestMapping
注解所标注url
与方法的绑定。
进一步<4>
遍历 methods
即方法与@RequestMapping映射集合,然后调用registerHandlerMethod ,逐个注册对应的 HandlerMethod
对象,其逻辑如下:
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
MappingRegistry
是Spring MVC
内部用来管理和存储请求映射的一个组件。它负责将HTTP
请求映射到对应的处理方法上。具体来看,MappingRegistry
主要用于管理HandlerMethod
和其对应的URL
路径映射。并提供了注册、查找和移除映射的方法,其主要组成部分如下所示:
MappingRegistry
内部成员变量
class MappingRegistry {
/**
* 注册表
*
* Key: Mapping
* Value:{@link MappingRegistration}(Mapping + HandlerMethod)
*/
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
}
registry
为注册表。其存存储了所有的URL
路径和HandlerMethod
的映射关系。其Value
所使用的MappingRegistration
对象其实是(Mapping + HandlerMethod
)的组合。具体存储内容如下所示:
(Ps:由于篇幅原因,有关MappingRegistry
中的注册逻辑此处暂时省略)
总结
总结里看,在SpringMVC
内部,当加载RequestMappingHandlerMapping
初始化完成后,会执行其父类的afterPropertiesSet
方法,其会扫描有@Controller
或者@RequestMapping
注解的类下面的方法。
进一步,如果方法上面有@RequestMapping
注解,则会为该方法创建对应的RequestMappingInfo
对象。并将所有的 RequestMappingInfo
对象和 Method
以及方法所在类,往 MappingRegistry
进行注册,会生成 HandlerMethod
处理器(Method
所有信息)对象。 并最终通过MappingRegistry
负责管理和存储请求映射,将 HTTP
请求映射到对应的处理方法上。其主要通过注册表 registry
存储所有的 URL
路径和 HandlerMethod
的映射关系。
这样一来当 Spring MVC
的DispatcherServlet
处理请求的时候,获取到对应的 HandlerMethod
处理器,就可以通过反射
的方式来执行对应的方法。
转载自:https://juejin.cn/post/7390686791777173531