从源码分析ContextLoaderListener的加载过程(带时序图)

写在前面

在工作中遇到一个问题,就是使用 SpringMVC自带定时器 时,发现了定时任务执行两次的问题。虽然问题当是是解决了。但是却给我留下来一个大大的疑问,为什么这样配置?

  • 是不是一定要使用 ContextLoaderListener

  • 是不是一定增加 <context-param /> 中名为 contextConfigLocation 的属性?

为了解决这个疑问,首先,我们需要理解 ContextLoaderListener 加载过程。

概述 ContextLoaderListener

ContextLoaderListener 实现了 ServletContextListener 接口,ServletContextListener 是 Java EE 标准接口之一,类似 Jetty,Tomcat,JBoss 等 java 容器启动时便会触发该接口的 contextInitialized。

ContextLoaderListener 是由 Spring 公司 提供的一个类,您需要做的就是在 J2EE Servlet 标准的 web.xml<web-app/> 声明一个 <listener/>

<listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

ContextLoaderListener 初始化过程

网上关于 ContextLoaderListener 加载过程的文章已经很多很全面了,所以我就结合排名第一的文章,画了一张时序图。如果你是电脑查看的,你可以右击图片,然后“在新标签页中打开图片” 查看大图。

总的来看有几个点很重要:

  1. contextInitialized 通知 Spring Web 应用程序 Servlet 上下文发生变更。

  2. 我们需要为 Spring Web 应用程序创建一个 WebApplicationContext,一般来说,这个实现类是 XmlWebApplicationContext

  3. 我们需要为 Spring 应用程序上下文创建一个 ConfigurableListableBeanFactory,一般来说,这个实现类是 DefaultListableBeanFactory

  4. 最后,在 Spring 容器加载 Bean 定义时,会调用 loadBeanDefinitionsconfigLocations 即配置文件路径下,读取和解析配置文件。

1. contextInitialized

首先,ContextLoaderListenerContextLoader 的子类,并且实现了 ServletContextListener 接口。

第三方 Servlet 容器主动调用 contextInitialized(ServletContextEvent event)。参数 ServletContextEvent 是一个事件类,用于通知 web 应用程序的 Servlet 上下文的更改。

2.1 实例化 Spring Web 应用程序上下文对象

紧接着调用 initWebApplicationContext 并且把 ServletContext 作为参数。接口 WebApplicationContextApplicationContext 的子类,是 Spring Web 应用程序的上下文。

createWebApplicationContext(ServletContextEvent event) 方法会 new 一个 WebApplicationContext 实例。具体的做法是先通过 determineContextClass 来决定应用上下文的类对象 contextClass,然后通过 BeanUtils.instantiateClass 来实例化对象。

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
      Class<?> contextClass = determineContextClass(sc);
      if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                  "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
      }
      // 主要是通过反射的方式,调用类默认构造函数创建实例
      // 这个方法跟主要流程关系不太大,所以就不具体展开说明了
      return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
determineContextClass

public class ContextLoader {
      public static final String CONTEXT_CLASS_PARAM = "contextClass";
      protected Class determineContextClass(ServletContext servletContext) {
            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);
                  }
            }
            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);
                  }
            }
      }
}
  

优先从 web.xml 中获取上下文参数,这里的<context-param />Servlet 的上下文参数,不是Spring 应用的上下文参数!但是这段代码实际效果是,在 Servlet 的上下文参数中,指定了接下来要创建的 Spring 应用的 ApplicationContext 的实现类名称。

<context-param>
      <param-name>contextClass</param-name>
      <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value>
</context-param>

<context-param /> 标签内的参数可以通过 Java 代码 ServletContext # getInitParameter 方法得到,具体的代码是由 Servlet 容器来实现的。

如果没有指定 contextClass 的名称,那么就会去获取默认的 contextClass 类名。默认 contextClass 类名需要从 defaultStrategies 中去获得。这个静态成员变量的初始化代码如下:

defaultStrategies 初始化

public class ContextLoader {
      private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
      private static final Properties defaultStrategies;
      static {
		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());
		}
	}
}
  

知道这个默认配置,也就是知道有这么一回事儿,学个思路。实际上这个配置是在打包的 jar 文件中的,我们开发者也没办法自定义。
有兴趣的可以点击展开看看。

ContextLoader.properties

ContextLoader.properties 在 jar 包中的路径

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

一般来说我们使用 Spring MVC 框架时,用到的都是 XmlWebApplicationContext,即解析 xml 文件生成 WebApplicationContext

2.2 配置和刷新 Spring Web 应用程序上下文

经过上一步,我们已经创建出一个 XmlWebApplicationContext,但是光有对象还是不够的,我们还需要configureAndRefreshWebApplicationContext来配置和刷新上下文。

第一段代码:设定 XmlWebApplicationContext 的 id

contextId 对于完整的流程而言只算一个小细节,有兴趣的可以点击展开

contextId

为 Spring Web 应用程序上下文设置一个更有意义的 ID


// 这是 configureAndRefreshWebApplicationContext 的第一块代码
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
	// The application context id is still set to its original default v
	// -> assign a more useful id based on available information
	String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
	if (idParam != null) {
		wac.setId(idParam);
	}
	else {
		// Generate default id...
                // 默认 contextId 前缀 + contextPath
		wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_
				ObjectUtils.getDisplayString(sc.getContextPath()));
	}
}
  

默认的 contextId 前缀是org.springframework.web.context.WebApplicationContext:


/**
 * Prefix for ApplicationContext ids that refer to context path and/or servlet name.
 */
String APPLICATION_CONTEXT_ID_PREFIX = WebApplicationContext.class.getName() + ":";
  

第二段代码:设定 XmlWebApplicationContext 的 configLocation

contextConfigLocation

// 这是 configureAndRefreshWebApplicationContext 的第二块代码
wac.setServletContext(sc);
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
      wac.setConfigLocation(configLocationParam);
}

从这段代码可以知道,我们可以用 <context-param /> 设置 contextConfigLocation 参数,为 WebApplicationContext 自定义配置文件的路径。

/**
 * Name of servlet context parameter (i.e., {@value}) that can specify the
 * config location for the root context, falling back to the implementation's
 * default otherwise.
 * @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
 */
 public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";

如果不自定义的话,默认会去加载 /WEB-INF/applicationContext.xml

/** Default config location for the root context. */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";

但是,这个 /WEB-INF/applicationContext.xml 还是需要你自己创建的,不然会抛出异常。

java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/applicationContext.xml]

最后一行:wac.refresh()

中间有配置环境变量的,有点复杂,等下次有机会遇到与之相关的问题再做分析。所以本次跳过了。

3. 创建 Bean 工厂

“破后而立” 之 refreshBeanFactory

public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext {
    // 刷新 Bean 工厂
    protected final void refreshBeanFactory() throws BeansException {
        // “破”:如果刷新时,Spring上下文实例中已经有一个Bean工厂了,那么就先销毁它
        if (this.hasBeanFactory()) {
            this.destroyBeans();
            this.closeBeanFactory();
        }

        try {
            // “立”:重新创建一个新的 Bean 工厂实例
            DefaultListableBeanFactory beanFactory = this.createBeanFactory();
            beanFactory.setSerializationId(this.getId());
            this.customizeBeanFactory(beanFactory);
            // 为 Bean 工厂加载 Bean 定义
            this.loadBeanDefinitions(beanFactory);
            // 为 Bean 工厂赋值新创建的工厂对象
            synchronized(this.beanFactoryMonitor) {
                this.beanFactory = beanFactory;
            }
        } catch (IOException var5) {
            throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5);
        }
    }      
}

4. 从 xml 配置文件中加载 Bean 定义

为 Bean 工厂加载 Bean 定义,本文使用的 xml 配置方式。XmlWebApplicationContext 用的是 XmlBeanDefinitionReader 来读取文件中的 Bean 定义。

public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
      // 接着上面的
      protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
		// Create a new XmlBeanDefinitionReader for the given BeanFactory.
		XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

		// Configure the bean definition reader with this context's
		// resource loading environment.
		beanDefinitionReader.setEnvironment(getEnvironment());
		beanDefinitionReader.setResourceLoader(this);
		beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

		// Allow a subclass to provide custom initialization of the reader,
		// then proceed with actually loading the bean definitions.
		initBeanDefinitionReader(beanDefinitionReader);
		loadBeanDefinitions(beanDefinitionReader);
	}
}

还记得上文中 ContextLoader # configureAndRefreshWebApplicationContext 中的那段代码吗?

String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
      wac.setConfigLocation(configLocationParam);
}

现在,<context-param/> 中配置的 contextConfigLocatin 就是在这里被使用的。

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
      String[] configLocations = getConfigLocations();
      if (configLocations != null) {
            for (String configLocation : configLocations) {
                  reader.loadBeanDefinitions(configLocation);
            }
      }
}

加载 Bean 定义的具体过程可以去查询 XmlWebApplicationContext # loadBeanDefinitions 方法。

参考文献

ContextLoaderListener 加载过程

Spring Web MVC - Other Web Frameworks - Common Configuration

原文地址:https://www.cnblogs.com/kendoziyu/p/ContextLoaderListener_contextInitialized.html