容器概述
最开始我们没有使用springboot的时候,还在使用web.xml进行配置的时候,就是在web中配置spring的参数来实现servlet和springMVC的集成。
一份古老的web.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!--指定Spring Bean的配置文件所在目录-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml
</param-value>
</context-param>
<!--配置了一个ContextLoaderListener-->
<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>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--配置自定义的Filter-->
<filter>
<filter-name>checkUserfilter</filter-name>
<filter-class>com.xxxx.common.util.CheckUserFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>checkUserfilter</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>checkUserfilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!--Spring MVC配置-->
<servlet>
<servlet-name>spring-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring-dispatcher</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
调试方法
其调试代码主要在:
org/springframework/web/context/ContextLoaderTests.java 类中,其主要入口方法在:testContextLoaderListenerWithDefaultContext
类图
加载容器的关键,在于配置的org.springframework.web.context.ContextLoaderListener其本身是实现了javax.servlet.ServletContextListener接口,通过其内部方法初始化容器
其类关系为:
其中两个接口都属于java的Servlet范畴
启动加载
从web.xml的配置中我们可以看到,WebApplicationContext 的加载主要是通过监听器(ContextLoaderListener )来进行的。服务器启动的时候会被监听器坚挺到,从而初始化Root WebApplicationContext 容器。那么先看看监听器的相关逻辑。
ContextLoaderListener
本身实现ServletContextListener 接口,主要负责Servlet的关闭和启动,进行WebApplicationContext 的初始化和销毁
监听器的地址: org/springframework/web/context/ContextLoaderListener.java
构造方法
public ContextLoaderListener() {
}
/**
* 可以直接传递一个 WebApplicationContext 对象,那样,
* 实际 ContextLoaderListener 就无需在创建一个新的 WebApplicationContext 对象
* 再其父类ContextLoader中 context直接被设置成了传入的参数
*/
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
其构造方法根据参数可以得知一个市负责创建WebApplicationContext ,另外一个是接收其他途径传递过来的WebApplicationContext 而不自己进行创建。
contextInitialized
// ContextLoaderListener.java
/**
* Initialize the root web application context.
* 初始化 WebApplicationContext 对象
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
// ContextLoader.java 具体初始化的方法在其父类ContextLoader中,下面有介绍
根据servlet的事件去进行WebApplicationContext的初始化
contextDestroyed
// ContextLoaderListener.java
/**
* Close the root web application context.
* 销毁 WebApplicationContext 容器的逻辑
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
// ContextLoader.java 具体销毁的方法在其父类ContextLoader中,下面有介绍
根据servlet的事件去进行WebApplicationContext的销毁,ContextCleanupListener.cleanupAttributes方法具体逻辑是从ServletContext获得属性名称,然后依次根据属性名称获取属性进行销毁。逻辑很简单,代码就不贴了,有兴趣可以自己看看
ContextLoader
ContextLoader是具体进行业务实现的相关类。 ContextLoader的地址: org/springframework/web/context/ContextLoader.java
构造方法
主要被分为两部分
- defaultStrategies 静态属性
- context 属性
defaultStrategies相关
这里是ContextLoader一些静态配置
public static final String CONTEXT_ID_PARAM = "contextId";
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
public static final String CONTEXT_CLASS_PARAM = "contextClass";
public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses";
public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses";
private static final String INIT_PARAM_DELIMITERS = ",; \t\n";
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
// 默认的配置 Properties 对象
private static final Properties defaultStrategies;
static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
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());
}
}
context相关
/**
* WebApplicationContext 对象
* The 'current' WebApplicationContext, if the ContextLoader class is
* deployed in the web app ClassLoader itself.
*/
@Nullable
private static volatile WebApplicationContext currentContext;
/**
* Root WebApplicationContext 对象
* The root WebApplicationContext instance that this loader manages.
*/
@Nullable
private WebApplicationContext context;
目前ContextLoader中存在的容器参数
initWebApplicationContext
进行WebApplicationContext 对象的初始化
/**
* 初始化 WebApplicationContext 对象
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 若已经存在 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 对应的 WebApplicationContext 对象,则抛出 IllegalStateException 异常。
// 例如,在 web.xml 中存在多个 ContextLoader 。
// 如果 web.xml 如果定义了多个 ContextLoader,会报错
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 {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
// 初始化 context ,即创建 context 对象
this.context = createWebApplicationContext(servletContext);
}
// 如果是 ConfigurableWebApplicationContext 的子类,如果未刷新,则进行配置和刷新
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
// 未刷新( 激活 )
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
// 无父容器,则进行加载和设置。
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
// 这是一个让子类实现的方法。当然
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置 context 对象,并进行刷新
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 记录在 servletContext 中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// 记录到 currentContext 或 currentContextPerThread 中
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
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");
}
// 返回 context
return this.context;
}
catch (RuntimeException | Error ex) {
// 当发生异常,记录异常到 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 中,不再重新初始化。
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
createWebApplicationContext
开始创建WebApplicationContext对象,在初始化容器的时候需要使用servletContext来构建WebApplicationContext的方法就是createWebApplicationContext
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 获得 context 的类
Class<?> contextClass = determineContextClass(sc);
// 判断 context 的类,是否符合 ConfigurableWebApplicationContext 的类型
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
// 创建 context 的类的对象
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
/**
* 获得 context 的类
*/
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);
}
}
}
configureAndRefreshWebApplicationContext
进行WebApplicationContext配置并刷新环境
// ContextLoader.java
// 配置 ConfigurableWebApplicationContext 对象,并进行刷新
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
// 如果 wac 使用了默认编号,则重新设置 id 属性
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
// 情况一,使用 contextId 属性
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
// 情况二,自动生成
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// 设置 context 的 ServletContext 属性
wac.setServletContext(sc);
// 设置 context 的配置文件地址
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
// 获得wac的环境,如果为配置,则初始化数据
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
// 执行自定义初始化 context
customizeContext(sc, wac);
// 刷新context
wac.refresh();
}
closeWebApplicationContext
进行容器关闭的操作
/**
* Close Spring's web application context for the given servlet context.
* <p>If overriding {@link #loadParentContext(ServletContext)}, you may have
* to override this method as well.
* @param servletContext the ServletContext that the WebApplicationContext runs in
* 关闭 WebApplicationContext 容器对象
*/
public void closeWebApplicationContext(ServletContext servletContext) {
servletContext.log("Closing Spring root WebApplicationContext");
try {
// 关闭 context
if (this.context instanceof ConfigurableWebApplicationContext) {
((ConfigurableWebApplicationContext) this.context).close();
}
}
finally {
// 移除 currentContext 或 currentContextPerThread
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = null;
}
else if (ccl != null) {
currentContextPerThread.remove(ccl);
}
// 从 ServletContext 中移除
servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
}
总结
其实WebApplicationContext 的加载内容还算清晰。
从web读取配置加载ContextLoaderListener并按部就班的初始化ServletContext内容,做好容器生命周期的监控。在springboot为主流的今天,配置web.xml的项目已经很少了。