Spring MVC 初始化源码—ContextLoaderListener监听器与根上下文容器的初始化
Posted L-Java
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC 初始化源码—ContextLoaderListener监听器与根上下文容器的初始化相关的知识,希望对你有一定的参考价值。
基于最新Spring 5.x,详细介绍了Spring MVC 初始化流程的源码,主要包括ContextLoaderListener与根上下文容器的初始化流程的源码,以及web.xml文件加载流程。
此前的一系列专栏文章中:Spring MVC 5.x 学习,我们对Spring MVC 5.x的重要特性进行了学习,基本掌握了Spring MVC的基本使用,现在我们一起来尝试学习Spring MVC的源码,尝试从源码的角度再次理解Spring MVC的整体执行流程,体会组件式架构的巧妙之处!
Spring MVC同样依赖于Spring,关于容器初始化、bean注册、对象创建等基础功能的具体源码,我们在此前的Spring源码学习部分已经花了几十万字详细讲解过了,在此不再赘述,在学习Spring MVC的源码之前建议大概了解Spring的源码。
本次主要学习web.xml文件加载流程以及ContextLoaderListener监听器的加载,即根上下文容器的初始化流程的源码。
下面的源码版本基于5.2.8.RELEASE
。
Spring MVC源码 系列文章
Spring MVC 初始化源码(1)—ContextLoaderListener与根上下文容器的初始化
文章目录
1 web.xml文件加载流程
引入Spring MVC之后就Java项目就成为了一个web项目,项目启动的流程相较于此前学习的本地Spring项目变得更加复杂,我们必须找到此时的项目初始化的入口,才能更好的进行分析。
我们的web项目实际上是一个非常被动的存在,里面没有main方法(非Spring Boot项目),它并不会自己启动,所谓的启动项目,是指的我的启动tomcat服务器,然后由tomcat服务器来对里面的web项目启动并且进行一系列初始化操作的。而tomcat服务器是通过加载项目的web.xml配置文件来启动整个项目的,因此,Spring MVC项目的启动流程可以从web.xml配置文件的加载过程中略知一二!
无论是原始Servlet的web项目,还是SSM的web项目,tomcat加载web.xml配置文件的过程和顺序都是一样的,常见组件的通用的加载顺序如下(部分顺序涉及到tomcat的源码,后面有机会我们在学习tomcat的源码):
tomcat
服务器首先会初始化该项目的Context容器StandardContext
,代表该web应用,并且会扫描web.xml文件中标签的数据并存入该容器的对应属性中,包括<context-param/>
标签表示的容器常量。- 根据扫描结果初始化
web.xml
中所有的定义的Listener
实例。 - 初始化项目中使用的(代码获取到的)
ServletContext
容器,实际类型是一个ApplicationContextFacade(基于外观模式)
。其内部封装了一个ApplicationContext
实例,ApplicationContext内部封装<context-param>
常量,还封装了tomcat内部的Context容器实例StandardContext
,可以获取tomcat内部注册的Servlet
等信息。 - 创建
ServletContextEvent
事件,其内部包含了ApplicationContextFacade
容器,随后发布该事件,即对所有的ServletContextListener实例调用contextInitialized方法
,可以从ServletContextEvent
中获取容器初始化参数信息。 - 根据扫描结果初始化
web.xml
中所有的定义的Filter
实例,并调用对应实例的init方法
初始化filter。 - 加载和初始化
load-on-startup属性大于等于0的Servelet
,按照属性值的大小从小到大依次加载和初始化,随后调用init方法
初始化Servlet,参数ServletConfig
实际是一个StandardWrapperFacade
类的对象,该类主要包含两个属性,config
对应ServletConfig
, 存储的实例为StandardWrapper
,context
对应ServletContext
,存储的实际为ApplicationContext
。
简单地说,主要加载顺序就是: Listener – SrvletContext – listener#contextInitialized(ServletContextEvent)– Filter – filter#init(FilterConfig) – 加载load-on-startup属性大于等于0的Servlet – Servlet#init(ServletConfig)
。
下面就是一个基于Spring MVC的web.xml一种最常见配置,可以对应着上面的流程看看这些标签的加载顺序。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Archetype Created Web Application</display-name>
<!--配置contextConfigLocation初始化参数,指定父容器Root WebApplicationContext的配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<!--加载全部配置文件-->
<param-value>classpath:spring-config.xml</param-value>
</context-param>
<!--监听contextConfigLocation参数并初始化父容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--设置编码-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!--对于request和response是否强制使用指定的编码-->
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<!--Servlet WebApplicationContext子容器的配置-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc-config.xml</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
实际上web.xml配置文件的<web-app/>
标签下可以配置很多子标签,这些标签在解析时都会被加载,但是很多标签我们都是用不到的,因此我们仅仅介绍这些常见标签的加载!
2 ContextLoaderListener根上下文容器初始化
Spring MVC项目支持父子容器,DispatcherServlet中初始化的容器作为子容器,通常用于存放三层架构中的表现层的bean,比如Controller,以及Spring MVC相关的组件bean实例,比如ViewResolver、HandlerMapping等,“子容器”一定会存在。
而父容器通常包含web应用中的基础结构 bean,例如需要跨多个Servlet实例共享的Dao、数据库配置bean、Service等服务bean,也就是三层架构中的业务层和持久层的bean,这些 bean可以在特定Servlet 的子 WebApplicationContext 中重写(即重新声明),ContextLoaderListener这个监听器可以配置,也可以不配置,通常情况下,该标签就被用于初始化一个父容器。
如果配置了ContextLoaderListener
监听器,那么将会有一个父容器被先初始化,我们来看看它的具体流程源码。
在ServletContext容器初始化之后,将会发出容器创建事件,随即触发ContextLoaderListener#contextInitialized(ServletContextEvent event)
方法调用,该方法就是我们的学习Sring MVC源码的入口:
/**
1. ContextLoaderListener的方法,源码的入口
2. <p>
3. 初始化一个 root WebApplicationContext
*/
@Override
public void contextInitialized(ServletContextEvent event) {
//调用ContextLoader的initWebApplicationContext方法
initWebApplicationContext(event.getServletContext());
}
其内部调用的就是ContextLoader的initWebApplicationContext
方法。该方法执行完毕,则项目的Root WebApplicationContext
初始化完毕。
2.1 initWebApplicationContext初始化根上下文容器
该方法还是很简单的,相比于单体项目的IOC容器的初始化,多了解析一些全局属性以及调用ApplicationContextInitializer扩展点的逻辑,大概逻辑如下:
- 校验如果上下文中的"
org.springframework.web.context.WebApplicationContext.ROOT
"属性值不为null的话,那么直接抛出异常。如果不为null,说明此前已经创建过root application context
容器了,不能再次创建。 - 如果此ContextLoader的context属性为
null
,那么调用createWebApplicationContext
方法初始化一个WebApplicationContext
,默认为XmlWebApplicationContext
。 - 调用
configureAndRefreshWebApplicationContext
方法配置并刷新新建的root WebApplicationContext
,该方法中会解析容器配置位置属性、初始化并调用ApplicationContextInitializer的扩展点(用于自定义root context)、执行refresh刷新容器的方法。 - 将当前新建的
Root WebApplicationContext
存入servletContext
的属性中,属性名为"org.springframework.web.context.WebApplicationContext.ROOT
",这也是开头校验的属性,该属性不为null就说明当前项目已经初始化好了Root WebApplicationContext
。
//ContextLoader的相关属性
/**
* 此加载程序管理的Root WebApplicationContext实例
*/
@Nullable
private WebApplicationContext context;
/**
* 如果当前初始化线程的ClassLoader本身就是ContextLoader,则将新建的容器上下文设置为当前的WebApplicationContext
*/
@Nullable
private static volatile WebApplicationContext currentContext;
/**
* ContextLoader的方法
* <p>
* 使用在构建时提供的ApplicationContext或根据contextClass和contextConfigLocation参数创建一个新的ApplicationContext。
* 为给定的ServletContext初始化Spring WebApplicationContext
*
* @param servletContext 当前ServletContext
* @return 新的 WebApplicationContext
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
/*
* 如果上下文中的"org.springframework.web.context.WebApplicationContext.ROOT"属性值不为null的话,那么直接抛出异常
* 如果不为null,说明此前已经创建过root application context容器了,不能再次创建
*/
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
//当前时间戳,毫秒
long startTime = System.currentTimeMillis();
try {
// 将上下文存储在本地实例变量中,以确保它在ServletContext关闭时可用。
/*
* 1 如果context属性为null,那么初始化一个WebApplicationContext,默认为XmlWebApplicationContext
*/
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
//如果属于ConfigurableWebApplicationContext类型,默认属于
if (this.context instanceof ConfigurableWebApplicationContext) {
//强制转换为cwac
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
//确定此应用程序上下文是否处于活动状态,即,是否至少刷新一次并且尚未关闭。
//如果上下文尚未刷新->提供诸如设置父上下文,设置应用程序上下文ID等服务。
if (!cwac.isActive()) {
// 如果父上下文为null
if (cwac.getParent() == null) {
// 确定根Web应用程序上下文的父级,。
//一般来说没有父上下文,因为ContextLoader的loadParentContext方法默认直接就是返回null的
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
/*
* 2 配置并刷新新建的WebApplicationContext,这是核心方法
*/
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
/*
* 3 将当前新建的Root WebApplicationContext 存入servletContext的属性中
* 属性名为"org.springframework.web.context.WebApplicationContext.ROOT"
*
* 这也是开头校验的属性,该属性不为null就说明当前项目已经初始化好了Root WebApplicationContext
*/
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
//获取当前初始化线程的ClassLoader,这个classLoader一般都是WebappClassLoader
//WebappClassLoader是tomcat提供的,每个web应用程序都有自己专用的WebappClassLoader,用于隔离web应用之间的class的影响
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
//如果当前初始化线程的ClassLoader本身就是ContextLoader的ClassLoader
//ContextLoader的ClassLoader同样也是WebappClassLoader,这也是tomcat设置的
if (ccl == ContextLoader.class.getClassLoader()) {
//则将新建的容器上下文设置为当前的WebApplicationContext
currentContext = this.context;
//如果不是并且不为null,那么将classLoader和context设置给一个map缓存
} else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
//输出日志
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
//最后返回创建、初始化完毕的root context
return this.context;
} catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
2.1.1 createWebApplicationContext创建新WebApplicationContext
如果当前ContextLoaderListener实例的context属性为null,那么调用createWebApplicationContext
方法初始化一个WebApplicationContext
,类型可以是默认上下文类XmlWebApplicationContext
,也可以是自定义上下文类(如果已指定)。
初始化容器时,调用的是无参构造器,此时可以说是仅仅创建了一个WebApplicationContext
对象,并没有进行一系列的初始化
操作。
/**
* ContextLoader的方法
* <p>
* 实例化此加载器的root WebApplicationContext,类型可以是默认上下文类,也可以是自定义上下文类(如果已指定)。
* <p>
* 指定的上下文类期望是实现了ConfigurableWebApplicationContext接口
* 另外,在刷新上下文之前会调用customContext方法,从而允许子类对上下文执行自定义修改。
*
* @param sc 当前ServletContext
* @return root WebApplicationContext
*/
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
//返回要使用的WebApplicationContext实现类的Class
Class<?> contextClass = determineContextClass(sc);
//如果对应的Class不是ConfigurableWebApplicationContext类型,那么抛出异常
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
//反射调用无参构造器初始化WebApplicationContext的实例并返回
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
2.1.1.1 determineContextClass获取上下文的Class
该方法获取要使用的上下文的Class,首先采用自定义
的WebApplicationContext类型,这是通过名为"contextClass
"的<context-param>
全局初始化参数指定的,如果没有该参数,那么将使用默认
的WebApplicationContext,即org.springframework.web.context.support.XmlWebApplicationContext
,这是在ContextLoader
同路径下的ContextLoader.properties
配置文件中定义的。
也就是说,我们可以通过在web.xml
文件中定义一个param-name
为contextClass
的<context-param/>
标签来指定自定义的容器类型,param-value
就是自定义的容器的全路径名字符串
。
//----------ContextLoader的属性和静态块-----------
/**
* 要使用的root WebApplicationContext实现类的配置参数:"contextClass"
* 通过该全局参数可以指定一个自定义WebApplicationContext实现类的全路径名
*/
public static final String CONTEXT_CLASS_PARAM = "contextClass";
/**
* 定义ContextLoader的默认策略名称的类路径资源的名称(相对于ContextLoader类)。
*/
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
/**
* 默认WebApplicationContext策略配置文件的属性集合
*/
private static final Properties defaultStrategies;
static {
// 从属性文件加载默认策略实现。
// 当前这严格是内部的文件,并不意味着应由应用程序开发人员自定义。
try {
//加载ContextLoader类路径下的ContextLoader.properties配置文件的键值对到defaultStrategies集合中
//该配置文件中定义了默ContextLoader的默认WebApplicationContext实现类
//默认实现类就是: org.springframework.web.context.support.XmlWebApplicationContext
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
} catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}
/**
1. ContextLoader的方法
2. <p>
3. 返回要使用的WebApplicationContext实现类
4. 如果未指定,则为默认XmlWebApplicationContext,或者是自定义的上下文类。
5. 6. @param servletContext 当前ServletContext
7. @return 使用的WebApplicationContext实现类
*/
protected Class<?> determineContextClass(ServletContext servletContext) {
//获取名为"contextClass"的全局参数,该参数用于指定自定义的WebApplicationContext
//一般都是没有指定的,即获取结果为null
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
//如果不为null,说明指定了该参数
if (contextClassName != null) {
try {
//获取自定义的WebApplicationContext的Class
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
//如果为null,说明没有指定该参数,将使用默认WebApplicationContext实现类
else {
//从defaultStrategies集合中获取名为org.springframework.web.context.WebApplicationContext的属性值
//默认实现类就是: org.springframework.web.context.support.XmlWebApplicationContext
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
//获取默认的XmlWebApplicationContext的Class
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}
ContextLoader.properties
如下:
2.1.1.2 XmlWebApplicationContext
XmlWebApplicationContext
是org.springframework.web.context.WebApplicationContext
的一种实现,该实现从XML文档获取配置。它的uml类图如下:
在最开始学习源码的时候我们就已经介绍了Spring的ApplicationContext体系,在此对于学习过的类不再赘述。
WebApplicationContext
继承了ApplicationContext接口,来自于spring-web
依赖,实现该接口的容器专门用于基于Servlet的web项目。该接口相比于ApplicationContext,多了一个getServletContext
方法,即获取Servlet上下文。因此,除了标准的ApplicationContext生命周期功能外,WebApplicationContext
的实现还需要检测ServletContextAware Bean
并相应地调用setServletContext
方法。
可配置的WebApplicationContext需要实现ConfigurableWebApplicationContext
的接口,该接口提供了设置ServletContext、ServletConfig、Namespace、ConfigLocation
的方法,可以对上下文进行自定义配置。
AbstractRefreshableWebApplicationContext
是ConfigurableWebApplicationContext的骨干实现,提供了各种属性用来保存配置的数据。
同时它还继承了AbstractRefreshableConfigApplicationContext
,因此是一个可刷新的ApplicationContext,继承了此前讲过的容器初始化的所有的功能。
XmlWebApplicationContext
是org.springframework.web.context.WebApplicationContext
的一种可用实现,该实现从XML文档获取配置。默认情况下,将从“/WEB-INF/applicationContext.xml
”获取根上下文的配置路径,从“/WEB-INF/test-servlet.xml
”获取具有“test-servlet
” 名称空间的子上下文的配置路径(例如servlet-name为“test”的DispatcherServlet
实例)。
可以通过org.springframework.web.context.ContextLoader
的contextConfigLocation
上下文参数(即全局<context-param/>
配置的contextConfigLocation
参数)和org.springframework.web.servlet.FrameworkServlet
的contextConfigLocation
参数(即servlet内部的<init-param/>
配置的contextConfigLocation
参数)覆盖配置位置的默认值。配置的位置值可以表示“/WEB-INF/context.xml
”之类的某个具体文件,也可以使用“/WEB-INF/*-context.xml
”之类的Ant
样式的模式匹配多个文件。
如果有多个配置位置,则较新的Bean定义将覆盖较早加载的文件中的定义,可以利用它来通过一个额外的XML文件有意覆盖某些bean定义。
2.1.2 configureAndRefreshWebApplicationContext配置并刷新容器
在创建了空容器之后(默认是XmlWebApplicationContext),将会调用configureAndRefreshWebApplicationContext方法配置并刷新该容器。
该方法执行完毕,则新建的WebApplicationContext容器配置并初始化完毕。
主要有如下步骤:
- 设置该应用程序
上下文的id
(一般用不到),默认id就是“org.springframework.web.context.WebApplicationContext:”+项目路径
,可以通过在web.xml
中配置名为contextId
的<context-param/>
全局参数来自定义Root容器id。 - 将
ServletContext
设置给该容器的servletContext
属性。 - 设置容器配置信息。首先获取名为
contextConfigLocation
的全局属性,如果配置了该属性,那么该属性的值将作为配置文件的路径,随后就调用setConfigLocation
方法解析传入的配置值,用以设置容器配置信息(配置值支持按照",; \\t\\n
"来拆分)。 - 获取容器的
Environment
环境变量对象,随后调用initPropertySources
方法手动初始化Servlet属性源
,该方法在refresh()
刷新容器的方法之前执行,以确保servlet属性源
已准备就绪,可以被refresh()
方法正常使用。 - 调用
customizeContext
方法用于对容器执行自定义操作。默认实现是通过web.xml
中配置的全局参数contextInitializerClasses和globalInitializerClasses
来确定指定了哪些ApplicationContextInitializer
类,并执行初始化,随后使用AnnotationAwareOrderComparator
排序(支持PriorityOrdered接口、Ordered接口、@Ordered注解、@Priority注解的排序),最后按照排序优先级从高到低一次调用每一个实例的initialize方法来初始化给定的servletContext
。 - 调用容器的
refresh
方法执行刷新
操作,这是核心方法
,我们在此前IoC容器初始化源码部分
已经着重讲解了。该方法将会初始化容器,包括解析配置文件,创建Spring bean实例,执行各种回调方法等等……操作(源码非常多)
。
//ContextLoader的常量属性
/**
* Root WebApplicationContext ID的配置参数,用作基础BeanFactory的序列化ID:"contextId"。
*/
public static final String CONTEXT_ID_PARAM = "contextId";
/**
* 指定Root WebApplicationContext的配置文件位置的servletContext参数的名称
* 即"contextConfigLocation",如果不存在该参数,则查找默认值。
*/
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
以上是关于Spring MVC 初始化源码—ContextLoaderListener监听器与根上下文容器的初始化的主要内容,如果未能解决你的问题,请参考以下文章
Spring MVC 初始化源码—@RequestMapping注解的源码解析
Spring MVC源码——Root WebApplicationContext