本篇,我打算从springMVC项目的web.xml的配置文件入手,通过部分源码逐步去理解解释三个容器的关系以及调用顺序,因为是基于我个人的理解,可能有所不足。
一般web.xml文件里会有如下两段配置信息:


我们先了解下web.xml,以下引用自 《web.xml文件是什么?有什么用?--详解》:
一般的web工程中都会用到web.xml,web.xml主要用来配置,可以方便的开发web工程。web.xml主要用来配置Filter、Listener、Servlet等。但是要说明的是web.xml并不是必须的,一个web工程可以没有web.xml文件。
1、WEB工程加载web.xml过程
经过个人测试,WEB工程加载顺序与元素节点在文件中的配置顺序无关。即不会因为 filter 写在 listener 的前面而会先加载 filter。WEB容器的加载顺序是:ServletContext -> context-param -> listener -> filter -> servlet。并且这些元素可以配置在文件中的任意位置。
加载过程顺序如下:
启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<listener>和<context-param>两个结点。
紧急着,容创建一个ServletContext(servlet上下文),这个web项目的所有部分都将共享这个上下文。
容器将<context-param>转换为键值对,并交给servletContext。
容器创建<listener>中的类实例,创建监听器。
然后结合我们上面的web.xml中关于spring和spring mvc的配置信息来进入话题:
首先,启动web容器的时,会先生成对应项目的ServelContent对象,这个是每个项目的上下文,这个ServelContent可以管理所有的servlet,并将我们web.xml中设置的<context-param>内容作为键值对交给这个对象。
然后加载<listener>标签内容,这个时候就会产生org.springframework.web.context.ContextLoaderListener。
spring的这个ContextLoaderListener在接下来的过程中很重要,我们来看一下源码

首先,可以看出它继承了ContextLoader类,并实现了ServletContextListener接口。
这里再直接引用他人的结论 《Spring中ContextLoaderListener作用》
继承ContextLoader有什么作用?
ContextLoaderListener可以指定在Web应用程序启动时载入Ioc容器,正是通过ContextLoader来实现的,可以说是Ioc容器的初始化工作。
实现ServletContextListener又有什么作用?
ServletContextListener接口里的函数会结合Web容器的生命周期被调用。因为ServletContextListener是ServletContext的监听者,如果ServletContext发生变化,会触发相应的事件,而监听器一直对事件监听,如果接收到了变化,就会做出预先设计好的相应动作。由于ServletContext变化而触发的监听器的响应具体包括:在服务器启动时,ServletContext被创建的时候,服务器关闭时,ServletContext将被销毁的时候等,相当于web的生命周期创建与效果的过程。
那么ContextLoaderListener的作用是什么?
ContextLoaderListener的作用就是启动Web容器时,读取在contextConfigLocation中定义的xml文件,自动装配ApplicationContext的配置信息,并产生WebApplicationContext对象,然后将这个对象放置在ServletContext的属性里,这样我们只要得到Servlet就可以得到WebApplicationContext对象,并利用这个对象访问spring容器管理的bean。简单来说,就是上面这段配置为项目提供了spring支持,初始化了Ioc容器。
好了,人家说法中回到我们的起点了,我们基本都被人告知“ContextLoaderListener的作用是创建并初始化spring容器”
那我们就可以深入进去看看,到底哪里做了这一步:
首先,我们知道了ServletContextListene是ServletContext的监听者,监听器的响应动作就是在服务器启动时contextInitialized会被调用,关闭的时候contextDestroyed被调用,这个好理解,那我们就来看一下ContextLoaderListener重写的contextInitialized方法到底做了什么。

我们再进入观察initWebApplicationContext方法细看



我因为自己消化过一遍,直接给出关键位置的方法说明——
1、首先是278行:创建了WebApplicationContext,我们可以理解为spring容器的壳子有了
this.context = createWebApplicationContext(servletContext);
2、其次是288和289行:对ApplicationContext加载了配置文件,并设置servletContext为WebApplicationContext的parent,到这一步,可以理解为我们的spring容器也就差不多成型了
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
3、接下来是294行:把ApplicationContext对象以键值对的形式存到servletContext中,这一步很关键,就是因为servletContext中存在这个键值对,所以其他内部成员可以通过servletContext访问到ApplicationContext,当然也能使用其管理的bean,而spring mvc则没有这样存在servletContext,所以我觉得正是这一步决定了子容器springmvc可以取用父容器内的bean,反着则不然。
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

接下来直到轮到我们的springmvc容器<servlet>标签内容
会生成控制org.springframework.web.servlet.DispatcherServlet,这是一个前端控制器,主要的内容我之前也有一篇文章做过自我记录
我们可以看到设置的
<load-on-startup>1</load-on-startup>
这个标签大概意思就是:
1、load-on-startup 元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。
2、它的值必须是一个整数,表示servlet被加载的先后顺序。
3、如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
4、如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,值越小,servlet的优先级越高,就越先被加载。值相同时,容器就会自己选择顺序来加载。
在DispatcherServlet的时候就根据springMVC容器容器的配置文件生成。
比如我这边就是
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/configs/spring/mvc-dispatcher-servlet.xml</param-value>
</init-param>
那顺序确定了,我们再看一下spring和spring mvc的父子关系哪里确定:
我们可以从下面3个截图看到dispatcherServlet的继承关系,同时,init方法用的是dispatcherServlet父类的父类的方法。



重点在于initServletBean()方法,经过追踪,我们找到该方法的最终实现又是在dispatcherServlet的父类FrameworkServlet中


其中涉及父子关系的实际是在219行的initWebApplicationContext()方法


initWebApplicationContext()方法主要用于创建或刷新WebApplicationContext实例,并对Servlet功能所使用的变量进行初始化。
从238行源码就可以看到,它获得ContextLoaderListener中初始化的rootContext,
在246行设置了父子关系的引用,也就是从这一点我们看到了spring和springMVC的父子关系!
并且,可以看到这只是一条单向的引用,spring中没有引用直接指向springMVC,也就是子类能找到父类,然而父类都不知道这个子类,父子容器之间内部对象调用关系更明了。
再通过构造函数和Servlet的contextAttribute属性查找ServletContext来进行webApplicationContext实例的初始化,最终。
这个方法内263行源码onRefresh(wac)方法是FrameworkServlet提供的模板方法,在子类,也就是我们的DispatcherServlet的onRefresh()方法中进行了重写。而在onRefresh()方法中调用了initStrategies()方法来完成初始化工作,初始化Spring MVC的9个组件。

顺序上的基本总结就是
1、Tomcat在启动时给每个Web应用创建一个全局的上下文环境,这个上下文就是ServletContext,其为后面的Spring容器提供环境。
2、Tomcat在启动过程中触发容器初始化事件,Spring的ContextLoaderListener会监听到这个事件,它的contextInitialized方法会被调用,在这个方法中,Spring会初始化全局的Spring根容器,这个就是Spring的IoC容器,IoC容器初始化完毕后,Spring将其存储到ServletContext中,便于以后来获取。
3、Tomcat在启动过程中还会扫描Servlet,一个Web应用中的Servlet可以有多个,以SpringMVC中的DispatcherServlet为例,这个Servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个Servlet请求。
4、Servlet会在容器启动时加载或延迟加载(根据启动级别设置数字)。延迟加载时,当第一个请求达到时,serlet容器发现对应Servlet还没有被实例化,就调用Servlet的init方法。
在spring MVC里
DispatcherServlet在初始化的时候会建立自己的容器,叫做SpringMVC 容器,用来持有Spring MVC相关的Bean。同时,Spring MVC还会通过ServletContext拿到Spring根容器,并将Spring根容器设为SpringMVC容器的父容器,请注意,Spring MVC容器可以访问父容器中的Bean,但是父容器不能访问子容器的Bean, 也就是说Spring根容器不能访问SpringMVC容器里的Bean。
说的通俗点就是,在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。