浅谈SpringMVC源码之SpringServletContainerInitializer的完整加载流程
Posted 默辨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈SpringMVC源码之SpringServletContainerInitializer的完整加载流程相关的知识,希望对你有一定的参考价值。
本文主要讲解的是SpringMVC不使用web.xml文件,而是使用JavaConfig的方式完成对SpringMVC配置的配置。即换一种方式完成DispatcherServlet等类的配置。
参考文章:SpringMVC 通过java类配置(不通过web.xml和xml 配置文件方式)
通过本文,你将了解到:
1. SpringMVC和Spring之间如何使用父子容器进行连接
2. SpringMVC的ServletContextListener和DispatcherServlet是在什么时候进行加载,以及加载了什么东西
一、JDK的SPI机制
首先你需要明白这个SPI机制,即了解我们的配置类是在什么地方被调用的
我看到网上有人写的挺详细的,如果你还不知道这个机制,可以参考参考这篇文章:深入理解SPI机制
总结下来就是一句话,JDK会自动加载META-INF/services目录下的类。
如JDBC中的:
又如本篇文章的重点SpringMVC:
我们的故事就从这里说起
二、SpringServletContainerInitializer
以onStartup方法为入口开始探索
1、@HandlesTypes(WebApplicationInitializer.class)
在类上有一个@HandlesTypes注解,该注解的作用为,获取到所有的实现了WebApplicationInitializer接口的类,然后赋值给onStartup方法的webAppInitializerClasses参数。官方话术为,获取当前类(SpringServletContainerInitializer)感兴趣的类(WebApplicationInitializer)信息。
2、WebApplicationInitializer
下图是WebApplicationInitializer接口的继承体系,
测试类可以直接忽略,并且只关注AbstractContextLoaderInitializer这条继承链(上面部分)就好了
前文介绍了@HandlesTypes注解的作用,最终的效果就是,在符合不是接口、不是抽象类等要求的基础上,执行实现了WebApplicationInitializer接口的类的onStartup方法。我们不难发现,系统默认的接口实现类都是不符合要求的,这就是我们通过JavaConfig方式配置的流程中有一步是继承抽象类AbstractAnnotationConfigDispatcherServletInitializer,然后重写对应的方法即可。
然后以我们自定义的类为起点,加载对应的onStartup方法,它会调用到它父类的父类中的onStartup方法(父类的父类中有实现)
该层级的设计是基于模板方法的设计模式进行设计的。为了方便查看,我根据类的关系和类中方法的作用整理成了下面的结构图:
由图我们可以得出:
- 我们需要自己去实现对应的父容器及子容器的配置信息(指定对应的配置类)
- AbstractContextLoaderInitializer类中完成ContextLoader的注册初始化
- AbstractDispatcherServletInitializer类中完成前端控制器DispatcherServlet的注册初始化
- 不算我们自定义的类,一共四层,每层完成指定的操作。上面的2、3两点就是其中的两层操作,第4层的操作为创建容器,但是创建容器的过程中需要获取配置信息,这个数据来源于我们自定义的类,你也可以理解为第五层
只要你明白了这个层级关系,剩下的代码就很简单了。一定要多理几遍这个关系,这对于后面的整体理解都起着至关重要的作用
3、AbstractDispatcherServletInitializer
主要的调用逻辑由该类的onStartup方法开始。
首先会调用父类的onStartup方法,完成父容器的初始化基础工作。主要工作为创建父容器、创建ContextLoaderListener
然后调用当前类的registerDispatcherServlet方法。主要工作为创建MVC子容器、创建DispatcherServlet
1)父类初始化步骤
初始化ContextLoaderListener的时候传入的参数为父容器。这在第三节的赋值阶段会再次使用到
代码片段在AbstractContextLoaderInitializer
2)当前类初始化步骤
创建DispatcherServlet方法的时候,传入的是子容器。
代码片段在AbstractDispatcherServletInitializer类中
3)创建子容器
即是去拿子类的配置文件(我们自己实现的config配置类),完成创建
代码片段在AbstractAnnotationConfigDispatcherServletInitializer类中
以上的过程,我们就可以等价于使用xml形式的配置Spring
这个过程中还有两个重点,分别是ContextLoaderListener和DispatcherServlet,我在后面单独讲解
三、ServletContextListener初始化
本节对应的是前面提到的ContextLoaderListener
前面只是完成对该类的实例化,即已经完成相关参数的赋值。在启动Tomcat的时候,根据Listener的生命周期,容器我们会调用ContextLoaderListener类中的contextInitialized方法,执行相关的逻辑。
1、contextInitialized
该方法主要功能为初始化父容器(根容器),并把父容器添加到servletContext的上下文中
代码片段在ContextLoader类中
2、configureAndRefreshWebApplicationContext
配置contextConfigLocation,启动父容器
代码片段在ContextLoader类中
至此,父容器相关的准备工作已经处理完成
四、HttpServlet初始化
本节对应的是前面的DispatcherServlet
我们都知道SpringMVC的底层就是封装了Servlet。以至于我们在编写Controller层接口的时候,不再是去继承HttpServlet 类,重写对应的doGet、doPost方法…
现在我们只需要添加一个@Controller注解,添加对应的@RequestMapping映射即可完成一个接口的编写。这一切的一切都要从Spring为我们编写的一个Servlet说起,他就是DispatcherServlet。而这个类就是我们前面在创建子容器的时候初始化出来的
查看该类的继承关系,我们可以得到如下的关系图谱。而对应的调用关系也从这里开始。
这里需要补充Servlet的生命周期说明
Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:
- Servlet 初始化后调用 init () 方法。
- Servlet 调用 service() 方法来处理客户端的请求。
- Servlet 销毁前调用 destroy() 方法。
- 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。
1、init
即我们Servlet方法的调用位置为init方法,此时跳转到HttpServletBean类的init方法
该代码片段在HttpServletBean类中
接下来的几步调用都比较常规
init() --> initServletBean() --> initWebApplicationContext()
2、initWebApplicationContext
该方法主要功能为:设置MVC子容器和根容器的父子关系。加载子容器
该代码片段在FrameworkServlet类中
3、configureAndRefreshWebApplicationContext
该类可以类比理解在Listener步骤的初始化容器步骤,毕竟方法的名字都是一样的,只是来自不同的类。Listener类中初始化父容器,Servlet类中初始化子容器(MVC的容器)
该步骤会注册监听器,注意只是完成注册,具体的监听器方法的调用位置不在这里。监听器调用的位置在fresh方法中
该代码片段在FrameworkServlet类中
4、ContextRefreshListener
监听器是一个十分重要的点,因为后面DispatcherServlet在处理器映射器、处理器适配器、视图解析器的数据转发的时候会用到这些初始化map中的数据
对应的方法跳转逻辑:onApplicationEvent --> onRefresh --> onRefresh --> initStrategies
该代码片段在FrameworkServlet类中
选择其中一个map数据进行讲解,下图是初始化HandlerMapping的数据:
- detectAllHandlerMappings默认为true,即进入第一个判断
- 获取容器中,实现了HandlerMapping接口的所有Bean,赋值给handlerMappings
- 即未来我们获取使用handlerMappings的时候,是有值的,赋值的位置就在这里
该代码片段在FrameworkServlet类中
我们可以看到HandlerMapping接口的实现中,就有我们熟悉的BeanNameUrlHandlerMapping和RequestMappingHandlerMapping
至此,SpringMVC所需要的前期准备工作大致已经完成。
以上是关于浅谈SpringMVC源码之SpringServletContainerInitializer的完整加载流程的主要内容,如果未能解决你的问题,请参考以下文章