1. 概述
随着 Spring Boot 逐步全面覆盖到我们的项目之中,我们已经基本忘却当年经典的 Servlet + Spring MVC 的组合,那让人熟悉的 web.xml
配置。而本文,我们想先抛开 Spring Boot 到一旁,回到从前,一起来看看 Servlet 是怎么和 Spring MVC 集成,怎么来初始化 Spring 容器的。
在开始看具体的源码实现之前,我们先一起来看看现在“陌生”的 web.xml
配置。代码如下:
<!-- 省略非关键的配置 --> |
[1]
处,配置了org.springframework.web.context.ContextLoaderListener
对象。这是一个javax.servlet.ServletContextListener
对象,会初始化一个Root Spring WebApplicationContext 容器。这个过程,详细解析,见 「3. Root WebApplicationContext 容器」 。[2]
处,配置了org.springframework.web.servlet.DispatcherServlet
对象。这是一个javax.servlet.http.HttpServlet
对象,它除了拦截我们制定的*.do
请求外,也会初始化一个属于它的 Spring WebApplicationContext 容器。并且,这个容器是以[1]
处的 Root 容器作为父容器。这个过程,详细解析,见 《精尽 Spring MVC 源码分析 —— 容器的初始化(二)之 Servlet WebApplicationContext 容器》 。- 可能胖友会有疑惑,为什么有了
[2]
创建了容器,还需要[1]
创建了容器呢?因为可以配置多个[2]
呀。当然,实际场景下,不太会配置多个[2]
。😈 - 再总结一次,
[1]
和[2]
分别会创建其对应的 Spring WebApplicationContext 容器,并且它们是父子容器的关系。
因为有些胖友可能没接触过 web.xml
配置,详细的可以看看:
- J-Jian 《Spring MVC 的 web.xml 配置详解》 更加推荐
- yaohong 《Spring MVC 配置文件 web.xml 详解各方总结》 更加详细
下面,我们就开始看详细的代码。
2. 如何调试
执行 ContextLoaderTests#testContextLoaderListenerWithDefaultContext()
单元测试方法,即可执行本文涉及的一些逻辑。
当然,ContextLoaderTests 还提供了其他单元测试方法,胖友可以自己去尝试。
3. Root WebApplicationContext 容器
在概述中,我们已经看到,Root WebApplicationContext 容器的初始化,通过 ContextLoaderListener 来实现。在 Servlet 容器启动时,例如 Tomcat、Jetty 启动,则会被 ContextLoaderListener 监听到,从而调用 #contextInitialized(ServletContextEvent event)
方法,初始化 Root WebApplicationContext 容器。
而 ContextLoaderListener 的类图如下:类图
3.1 ContextLoaderListener
org.springframework.web.context.ContextLoaderListener
,实现 ServletContextListener 接口,继承 ContextLoader 类,实现 Servlet 容器启动和关闭时,分别初始化和销毁 WebApplicationContext 容器。
注意,这个 ContextLoaderListener 类,是在
spring-web
项目中。
3.1.1 构造方法
// ContextLoaderListener.java |
这两个构造方法,是因为父类 ContextLoader 有这两个构造方法,所以必须重新定义。比较需要注意的是,第二个构造方法,可以直接传递一个 WebApplicationContext 对象,那样,实际 ContextLoaderListener 就无需在创建一个新的 WebApplicationContext 对象。😈
3.1.2 contextInitialized
// ContextLoaderListener.java |
- 调用父类 ContextLoader 的
#initWebApplicationContext(ServletContext servletContext)
方法,初始化 WebApplicationContext 对象。详细解析,见 「3.2 ContextLoader」 。
3.1.3 contextDestroyed
// ContextLoaderListener.java |
- 销毁 WebApplicationContext 容器的逻辑。本文,甚至本系列,都应该暂时不会详细解析。所以,感兴趣的胖友,需要自己研究咯。当然,在这并不着急。
3.2 ContextLoader
org.springframework.web.context.ContextLoader
,真正实现初始化和销毁 WebApplicationContext 容器的逻辑的类。
注意,这个 ContextLoaderListener 类,是在
spring-web
项目中。
3.2.1 构造方法
因为 ContextLoader 的属性比较多,我们逐块来看。
第一块,defaultStrategies
静态属性,默认的配置 Properties 对象。代码如下:
// ContextLoader.java |
从
ContextLoader.properties
中,读取默认的配置 Properties 对象。实际上,正如Load default strategy implementations from properties file. This is currently strictly internal and not meant to be customized by application developers.
所注释,这是一个应用开发者无需关心的配置,而是 Spring 框架自身所定义的。打开来瞅瞅,代码如下:// spring-framework/spring-web/src/main/resources/org/springframework/web/context/ContextLoader.properties
# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext- 这意味着什么呢?如果我们没有在
<context-param />
标签中,配置指定的 WebApplicationContext 类型,就使用 XmlWebApplicationContext 类。😈 一般情况下,我们也不会主动指定。
- 这意味着什么呢?如果我们没有在
第二块,context
属性,Root WebApplicationContext 对象。代码如下:
// ContextLoader.java |
- 在下文中,我们将会看到,如果
context
是直接传入,则不会进行初始化,重新创建。😈
实际上,ContextLoader 还有其它属性,因为非关键,所以就不赘述。
3.2.2 initWebApplicationContext
#initWebApplicationContext(ServletContext servletContext)
方法,初始化 WebApplicationContext 对象。代码如下:
// ContextLoader.java |
<1>
处,若已经存在ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
对应的 WebApplicationContext 对象,则抛出 IllegalStateException 异常。例如,在web.xml
中存在多个 ContextLoader 。<2>
处,打印日志。<3>
处,调用#createWebApplicationContext(ServletContext sc)
方法,初始化context
,即创建 WebApplicationContext 对象。详细解析,胖友先跳到 「3.2.3 createWebApplicationContext」 。<4>
处,如果context
是 ConfigurableWebApplicationContext 的子类,如果未刷新,则进行配置和刷新。<4.1>
处,如果未未刷新( 激活 )。默认情况下,是符合这个条件的,所以会往下执行。😈<4.2>
处,无父容器,则进行加载和设置。默认情况下,#loadParentContext(ServletContext servletContext)
方法,返回null
。代码如下:// ContextLoader.java
protected ApplicationContext loadParentContext(ServletContext servletContext) {
return null;
}- 这是一个让子类实现的方法。当然,子类 ContextLoaderListener 并没有重写该方法。所以,实际上,
<4.2>
处的逻辑,可以暂时忽略。
- 这是一个让子类实现的方法。当然,子类 ContextLoaderListener 并没有重写该方法。所以,实际上,
<4.3>
处,调用#configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)
方法,配置 ConfigurableWebApplicationContext 对象,并进行刷新。详细解析,胖友先跳到 「3.2.4 configureAndRefreshWebApplicationContext」 中。
<5>
处,记录context
在 ServletContext 中。这样,如果web.xml
如果定义了多个 ContextLoader ,就会在<1>
处报错。<6>
处,记录到currentContext
或currentContextPerThread
中,差异在于类加载器的不同。变量代码如下:// ContextLoader.java
/**
* Map from (thread context) ClassLoader to corresponding 'current' WebApplicationContext.
*/
private static final Map<ClassLoader, WebApplicationContext> currentContextPerThread =
new ConcurrentHashMap<>(1);
/**
* The 'current' WebApplicationContext, if the ContextLoader class is
* deployed in the web app ClassLoader itself.
*/
private static volatile WebApplicationContext currentContext;- 目前这两个变量,在销毁 Spring WebApplicationContext 容器时会用到。暂时还看不太出其它的用途。搜索网络上的文章,也没人细说这个。如果知道的胖友,麻烦告知下。
<7>
处,打印日志。<8>
处,返回context
。<9>
处,当发生异常,记录异常到WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
中,不再重新初始化。即对应到<1>
处的逻辑。
3.2.3 createWebApplicationContext
#createWebApplicationContext(ServletContext sc)
方法,初始化 context
,即创建 WebApplicationContext 对象。代码如下:
// ContextLoader.java |
<1>
处,调用#determineContextClass(ServletContext servletContext)
方法,获得context
的类。代码如下:// ContextLoader.java
/**
* Config param for the root WebApplicationContext implementation class to use: {@value}.
* @see #determineContextClass(ServletContext)
*/
public static final String CONTEXT_CLASS_PARAM = "contextClass";
protected Class<?> determineContextClass(ServletContext servletContext) {
// 获得参数 contextClass 的值
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
// 情况一,如果值非空,则获得该类
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
// 情况二,从 defaultStrategies 获得该类
} else {
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}- 分成两种情况。前者,从 ServletContext 配置的
context
类;后者,从ContextLoader.properties
配置的context
类。 - 默认情况下,我们不会主动在 ServletContext 配置的
context
类,所以基本是使用ContextLoader.properties
配置的context
类,即 XmlWebApplicationContext 类。
- 分成两种情况。前者,从 ServletContext 配置的
<2>
处,判断context
的类,是否符合 ConfigurableWebApplicationContext 的类型。显然,XmlWebApplicationContext 是符合条件的,所以不会抛出 ApplicationContextException 异常。<3>
处,调用BeanUtils#instantiateClass(Class<T> clazz)
方法,创建context
的类的对象。
😈 回到 「3.2.2 initWebApplicationContext」 ,我们继续撸。
3.2.4 configureAndRefreshWebApplicationContext
#configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)
方法,配置 ConfigurableWebApplicationContext 对象,并进行刷新。代码如下:
// ContextLoader.java |
- 此处,注释上即写了
wac
,右写了context
,实际上,是等价的东西。下面的文字,我们统一用wac
。 <1>
处,如果wac
使用了默认编号,则重新设置id
属性。默认情况下,我们不会对wac
设置编号,所以会执行进去。而实际上,id
的生成规则,也分成使用contextId
在<context-param />
标签中设置,和自动生成两种情况。😈 默认情况下,会走第二种情况。<2>
处,设置wac
的 ServletContext 属性。【关键】
<3>
处,设置context
的配置文件地址。例如我们在 「1. 概述」 中所看到的。<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:config/applicationContext.xml</param-value>
</context-param><4>
处,TODO 芋艿,暂时忽略。非关键<5>
处,调用#customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac)
方法,执行自定义初始化wac
。非关键方法,先直接略过。感兴趣的胖友,自己去瞅瞅。- 【关键】
<6>
处, 刷新wac
,执行初始化。此处,就会进行一些的 Spring 容器的初始化。可能胖友对 Spring IOC 不是很了解,所以后面可以回过头看 《精尽 Spring 源码》 。
😈 回到 「3.2.2 initWebApplicationContext」 ,我们继续撸。
3.2.5 closeWebApplicationContext
#closeWebApplicationContext(ServletContext servletContext)
方法,关闭 WebApplicationContext 容器对象。代码如下:
// ContextLoader.java |
- 不详细解析,胖友自己瞅瞅。
666. 彩蛋
下一篇,我们一起来看看 Servlet WebApplicationContext 容器是怎么初始化的。
参考和推荐如下文章: