浅谈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方法(父类的父类中有实现)



该层级的设计是基于模板方法的设计模式进行设计的。为了方便查看,我根据类的关系和类中方法的作用整理成了下面的结构图:


由图我们可以得出:

  1. 我们需要自己去实现对应的父容器及子容器的配置信息(指定对应的配置类)
  2. AbstractContextLoaderInitializer类中完成ContextLoader的注册初始化
  3. AbstractDispatcherServletInitializer类中完成前端控制器DispatcherServlet的注册初始化
  4. 不算我们自定义的类,一共四层,每层完成指定的操作。上面的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的数据:

  1. detectAllHandlerMappings默认为true,即进入第一个判断
  2. 获取容器中,实现了HandlerMapping接口的所有Bean,赋值给handlerMappings
  3. 即未来我们获取使用handlerMappings的时候,是有值的,赋值的位置就在这里

该代码片段在FrameworkServlet类中


我们可以看到HandlerMapping接口的实现中,就有我们熟悉的BeanNameUrlHandlerMapping和RequestMappingHandlerMapping

至此,SpringMVC所需要的前期准备工作大致已经完成。

以上是关于浅谈SpringMVC源码之SpringServletContainerInitializer的完整加载流程的主要内容,如果未能解决你的问题,请参考以下文章

秒懂SpringBoot之Mvc请求执行流程浅谈

浅谈SpringMVC核心组件及执行流程(含源码解析)

浅谈SpringMVC核心组件及执行流程(含源码解析)

浅谈SpringMVC源码的DispatcherServlet组件执行流程

浅谈责任链设计模式在框架源码中的运用

浅谈 qmake 之 shadow build(就是将源码路径和构建路径分开)