文章目录
- 一、Tomcat如何启动两个容器—Spring容器、SpringMVC容器
- 二、处理@RequestMapping注解,建立url和方法的关系
- 三、初始化DispacherServlet
- 四、总结
- 五、扩展
- 项目git地址及相关链接
接上篇,我们实现了一个简单的SpringMVC小程序。麻雀虽小,五脏俱全,里面包含了一个web工程基本的组件。现在我们开始剖析源码,分为如下几个步骤
1.Tomcat如何启动两个容器—spring容器、SpringMVC容器
2.处理@RequestMapping注解,建立<url,controller(中的方法)>关系。
3.初始化dispacherServlet
4.dispacherServlet如何处理请求(前三步都是为该步骤打下基础,具体内容放到下一篇)
注Spring的版本是5.2.12.RELEASE,不同版本略有差异,但核心逻辑是不变的。
一、Tomcat如何启动两个容器—spring容器、SpringMVC容器
前面我们提到web项目会启动两个容器,两个容器启动过程中有一个共同点就是,初始化的方法位于AbstractApplicationContext类的refresh方法。
1.先启动刷新spring容器
该功能由ContextLoader类的initWebApplicationContext方法实现,省略一些次要代码
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
try {
if (this.context == null) {
//1.创建spring容器
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
//2.刷新该容器,调用了AbstractApplicationContext类的refresh方法
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
return this.context;
}
}
栈调用如下图
看下刷新完后的spring容器中bean
2.接着启动刷新SpringMVC容器
该功能主要由FrameworkServlet类的createWebApplicationContext方法实现
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
//1.创建容器实例
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
//2.设置刚才创建的spring容器为父容器。父容器可以在ServletContext中拿到
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
3.刷新容器
configureAndRefreshWebApplicationContext(wac);
return wac;
}
刷新完的SpringMVC容器,只包含了UserController的实例,没有UserService的实例。图有点长,就不截了,大家可以自己验证。
3.父子容器描述
1.概括上述步骤
上述步骤可以总结成下图
简单来说就是初始化listener时初始化spring容器,初始化Servlet时初始化SpringMVC容器。
需要说明的是,在本例中父子容器其实都是XmlWebApplicationContext的实例,只是两个实例做了一个引用关系
2.这个过程需要注意以下几个点
如果把bean全部放到spring容器会怎么样?
如果将controller的bean都放入的spring容器中,如果访问该项目会出现404的错误。
因为在解析@RequestMapping过程中,initHandlerMethods()方法只是对SpringMVC容器中的bean进行处理的,并没有找入父容器的bean。此时url和controller中的方法(或者是handler)没有生成对应关系,也就是说没有方法能够处理该请求,出现404的错误。
如果把bean全部放在SpringMVC容器会怎么样?
在使用事务时会导致失效
3.为什么要分父子容器
SpringMVC是从struct发展过来的,也就是说方便两者进行切换,而不用动spring容器中的bean。
二、处理@RequestMapping注解,建立url和方法的关系
上面提的关系就是<url,controller(中的方法)>Map结构。要注意的是第二部分是在SpringMVC容器初始化过程中完成。
1.说下过程中涉及的类
- RequestMappingInfo: 这个类是对请求映射的一个抽象,它包含了请求路径,请求方法,请求头等信息。其实可以看做是@RequestMapping的一个对应类。
- HandlerMethod: 这个类封装了处理器实例(Controller bean)和处理方法实例(Method)以及方法参数数组(MethodParameter[])
- HandlerMapping :该接口的实现类用来定义请求和处理器之前的映射关系,其中定义了一个方法getHandler。
- AbstractHandlerMethodMapping :这是HandlerMapping的一个基本实现类,该类定义了请求与HandlerMethod实例的映射关系。
- RequestMappingHandlerMapping: 它将@RequestMapping注解转化为RequestMappingInfo实例,并为父类使用。
2.怎么将上面的类串联起来呢?分为三个步骤
-
1.遍历注解:SpringMVC容器初始化过程中,具体在创建RequestMappingHandlerMapping类实例时,对标注了@Controller或@RequestMapping注解的类中方法进行遍历。
-
2.封装RequestMappingInfo实例:将类和方法上的**@RequestMapping注解值进行合并,封装成一个RequestMappingInfo实例**。以controller中的方法对象为key,对应的RequestMappingInfo实例为value存入map中,遍历map开始注册
-
3.注册到Map中:将这个Controller实例、方法及方法参数信息封装到HandlerMethod中。以RequestMappingInfo为key,HandlerMethod为value存储到map结构。将url(@RequestMapping注解value值)为key,以RequestMappingInfo为value存到map结构中
3.上面步骤实现细节
1.遍历注解
入口开始在RequestMappingHandlerMapping类的afterPropertiesSet方法
//创建实例过程中回调此方法
public void afterPropertiesSet() {
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());
//调用父类方法
super.afterPropertiesSet();
}
回调父类AbstractHandlerMethodMapping的afterPropertiesSet方法。
下面的方法都是在AbstractHandlerMethodMapping类中,这个要有印象,在下一篇dispacherServlet逻辑处理中要用到
public void afterPropertiesSet() {
initHandlerMethods();
}
//1.扫描SpringMVC容器中所有bean,检测并注册Handler方法
protected void initHandlerMethods() {
//1.1扫描所有bean
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(ScopED_TARGET_NAME_PREFIX)) {
//1.2处理不是以"scopedTarget."开头的bean
processCandidateBean(beanName);
}
}
//不用管它
handlerMethodsInitialized(getHandlerMethods());
}
进入父类processCandidateBean方法,从方法名字可以看出来这个是处理候选bean方法。
怎么处理呢?
在这个方法里只是判断下是不是Handler通过isHandler(beanType),判断的条件就是这个bean上有没有@Controller或者@RequestMapping注解。
如果有则开始从bean中发现url和方法的对应关系,实现这个方法的是detectHandlerMethods
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
2.封装成一个RequestMappingInfo实例。
进入detectHandlerMethods方法,这个方法完成了两件事
1.得到一个Map<Method, T> methods变量,Method就是controller中的方法,T代表的是RequestMappingInfo
2.遍历methods进行注册
protected void detectHandlerMethods(Object handler) {
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
1.完成第一件事儿
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
//2.完成第二件事儿
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectinvocableMethod(method, userType);
//开始注册
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
我们看下methods的存储的数据
3.注册就是向两个Map结构中存储数据
此时进入内部类MappingRegistry,根据类名可以推测出来是注册映射关系的。介绍两个成员变量
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
mappingLookup:以RequestMappingInfo为key,HandlerMethod为value的Map结构
urlLookup:以url为key,RequestMapping集合为value的Map结构
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
public void register(T mapping, Object handler, Method method) {
//省略一下不重要代码
//增加加锁性能,只加了写锁
this.readWriteLock.writeLock().lock();
try {
//根据handler(就是Controller实例)和controller中的方法对象生成HandlerMethod
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
validateMethodMapping(handlerMethod, mapping);
1.向第一个Map存值
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
//2.向第二个Map存值
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);
}
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}
看下mappingLookup存完值的图
urlLookup存完值的图
现在已经向Map中存储了数据,怎么根据一个请求找到对应的方法呢?就两个步骤
-
1.根据请求的url,从urlLookup取出value(RequestMappingInfo实例)
-
2.根据RequestMappingInfo实例从mappingLookup取出对应的HandlerMethod
上面的步骤就是下一篇dispacherServlet要写的根据url找到对应Controller中的方法。
三、初始化dispacherServlet
上面的过程描述了Tomcat是怎么初始化两个容器的,下面叙述下在初始化完SpringMVC容器后,如何给这个容器添加上web的全局变量,所谓的添加web的全局变量就是给dispacherServlet中的成员变量赋值,这个功能是onRefresh方法实现的。
1.onRefresh方法
**_dispacherServlet类 _**重写FrameworkServlet的onRefresh方法,主要的刷新功能放到了initStrategies来做。
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
//1.初始化MultipartResolver,主要用于处理文件上传
initMultipartResolver(context);
//2.初始化LocaleResolver,实现国际化
initLocaleResolver(context);
//3.初始化主题,控制网页风格
initThemeResolver(context);
//4.初始化HandlerMappings
initHandlerMappings(context);
//5.初始化HandlerAdapters
initHandlerAdapters(context);
//6.初始化异常处理器
initHandlerExceptionResolvers(context);
//7.初始化请求解析视图翻译器,为请求找到一个合适的View
initRequestToViewNameTranslator(context);
//8.初始化视图解析器
initViewResolvers(context);
//9.初始化FlashMapManager,闪存管理器。可以缓存请求的属性值交给下一个请求,再清空缓存
initFlashMapManager(context);
}
initHandlerMappings(context)
这个方法主要就是给dispacherServlet中 List handlerMappings;属性赋值。
怎么赋值呢?
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
//1.找到IOC容器中所有HandlerMapping类型的bean,放到map中
Map<String, HandlerMapping> matchingBeans =
beanfactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
//2.判空后就赋值给handlerMappings属性
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
//省略判断
}
在IOC容器中找到的bean如下所示
这些bean是在SpringMVC容器初始化时加载当前系统中所有实现了HandlerMapping接口的bean。
initHandlerAdapters(context)
该方法和初始化HandlerMapping方法如出一辙,都是从SpringMVC容器中找到 HandlerAdapter类型的bean然后给List handlerAdapters赋值
private void initHandlerAdapters(ApplicationContext context) {
this.handlerAdapters = null;
if (this.detectAllHandlerAdapters) {
//1.找到IOC容器中所有HandlerAdapter类型的bean,放到map中
Map<String, HandlerAdapter> matchingBeans =
beanfactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
if (!matchingBeans.isEmpty()) {
//2.判空后就赋值给handlerAdapters属性
this.handlerAdapters = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
//不太重要省略掉
}
看下从SpringMVC容器找到的bean
2.总结给成员变量赋值方法
仔细看下dispacherServlet中这几个nitxxxxx方法里面有几个共同点
1.代码是有大量重复代码的,不变的是赋值的dispacherServlet成员变量不同,如this.handlerAdapters = null
2.而且有一个共同的逻辑就是如果SpringMVC容器没有成员变量对应的bean,那么就会加载dispacherServlet配置文件中的bean
dispacherServlet配置文件dispacherServlet.properties内容如下
//实现国际化
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
//主题
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
//HandlerMapping
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
org.springframework.web.servlet.function.support.RouterFunctionMapping
//HandlerAdapter
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
org.springframework.web.servlet.function.support.HandlerFunctionAdapter
//HandlerExceptionResolver
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
//RequestToViewNameTranslator
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
//ViewResolver
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
//FlashMapManager
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
什么时候加载这个配置文件呢?在dispacherServlet类加载的时候,位于静态代码块中
static {
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, dispatcherServlet.class);
//赋值给成员变量
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
}
//如果没有从SpringMVC容器中找到各个bean,则从配置文件的成员变量中找
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from dispatcherServlet.properties");
}
}
如果在mvc的配置文件中有如下配置,则不会走上面的判断。
<mvc:annotation-driven></mvc:annotation-driven>
该配置的作用就是将配置文件的各个bean注册到SpringMVC容器中。
四、总结
上面最主要的点就是当一个项目启动时,会初始化两个容器(spring容器和SpringMVC容器),为了完成web项目处理请求的功能,需要为SpringMVC容器添加web的全局变量,其实就是为dispacherServlet的成员变量赋值。
五、扩展
本来写到上面就可以停笔了,出于好奇验证了下现在SpringBoot还采用父子容器形式了吗?发现不是。
下面源码基于SpringBoot 2.1.1.RELEASE 版本,可能版本间使用的IOC容器不相同,大家可以自行验证。
验证如下
从SpringBoot的main函数进去,一直到SpringApplication类的run方法,该方法中有一行去创建容器
context = createApplicationContext();
在该行代码打上断点运行

此时的容器只有一个就是**AnnotationConfigServletWebServerApplicationContext**,继续向下走,去刷新容器。
查看该容器中的单例池(beanfactory属性中的singletonObjects)是否包含controller和service包下的类实例。
涉及到的对象有点多,只截取重要的信息

由上图可以发现controller和service以及视图解析器都在单例池中。
可以证明SpringBoot只使用了一个容器
参考文章
mvc:annotation-driven的作用
Spring和SpringMVC父子容器概念
SpringMvc在SpringBoot环境和Web环境中上下文的关系
项目git地址及相关链接
项目git地址:https://gitee.com/shang_jun_shu/springmvc-analysis
下篇:
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。