概述
Spring在web應用中的預設容器類為XmlWebApplicationContext,這個容器類通過xml檔案擷取所有的配置資訊。它的繼承結構如下圖,(點此檢視大圖)
在web應用中,不管是ContextLoaderListener,還是DispatcherServlet初始化的時候,都是以XmlWebApplicationContext為預設容器。在下面的研究中,我将以ContextLoaderListener的初始化過程介紹spring容器在web應用的初始化。
ContextLoaderListener的初始化過程中最主要的任務時加載spring容器,并把此容器加入到ServletContext中作為整個web應用的跟容器。ContextLoaderListener加載spring容器大緻分為兩個階段,第一個階段是解析web.xml檔案中的初始化參數以對spring容器做定制化操作,簡單的說就是定制spring容器;第二階段是spring容器的重新整理過程。下面分别對這兩個階段進行探讨。
第一階段 定制spring容器
ContextLoaderListener實作了ServletContextListener,是以web容器啟動的時候就會執行它的contextInitialized方法,此方法的代碼如下。
public void contextInitialized(ServletContextEvent event) {
// 擷取ContextLoader對象
this.contextLoader = createContextLoader();
if (this.contextLoader == null) {
this.contextLoader = this;
}
this.contextLoader.initWebApplicationContext(event.getServletContext());
}
@Override
public void contextInitialized(ServletContextEvent event) {
// 執行父類ContextLoader的initWebApplicationContext方法
initWebApplicationContext(event.getServletContext());
}
這段代碼主要是調用父類ContextLoader的initWebApplicationContext(ServletContext servletContext)方法,下面是initWebApplicationContext方法在ContextLoader類中的代碼。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 檢查ServletContext是否已經有了根容器
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!");
}
Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
if (this.context == null) {
// 建立容器,據contextClass初始化參數指定或者使用預設的XmlWebApplicationContext類
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// 如果容器沒有被重新整理,執行以下操作
// 設定父容器
if (cwac.getParent() == null) {
// 加載父容器。
// 通過locatorFactorySelector上下文初始化參數指定父容器所在的配置檔案路徑
// 通過parentContextKey上下文初始化參數指定父容器的名稱
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置并執行容器重新整理操作
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 把spring容器加入到ServletContext中作為根容器
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
} else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isDebugEnabled()) {
logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
}
return this.context;
} catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
} catch (Error err) {
logger.error("Context initialization failed", err);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
throw err;
}
}
這段代碼分成4步,首先通過調用ContextLoader的createWebApplicationContext(ServletContext sc)方法來建立spring容器,然後設定spring容器的父容器,接着調用ContextLoader的configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)方法來配置并重新整理spring容器,最後把容器儲存到servlet容器中。最後一步的代碼已經在上面展現了,下面我們來解析前三步的代碼。
1. 建立spring容器。
調用ContextLoader的createWebApplicationContext(ServletContext sc)方法,這個方法的代碼如下。
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);
}
上面主要是通過ContextLoader的determineContextClass(ServletContext servletContext)方法擷取容器類對象,然後執行個體化容器。下面是determineContextClass方法的代碼。
/**
* 擷取容器類對象
**/
protected Class<?> determineContextClass(ServletContext servletContext) {
// 聲明:public static final String CONTEXT_CLASS_PARAM = "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);
}
} 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);
}
}
}
determineContextClass從ServletContext對象中擷取contextClass初始化參數的值,如果這個參數有值,則使用這個參數指定的容器類,否則使用預設的容器類XmlWebApplicationContext。如果不使用spring的預設容器,可以在web.xml中通過配置contextClass指定其他容器類,比如。
<!-- 定義contextClass參數 -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
com.chyohn.context.XmlWebApplicationContext
</param-value>
</context-param>
2. 指定父容器。
建立完spring容器後,initWebApplicationContext中會調用ContextLoader的 loadParentContext(ServletContext servletContext)方法來擷取父容器,并把這個父容器與剛建立的容器關聯上。loadParentContext方法的代碼如下。
protected ApplicationContext loadParentContext(ServletContext servletContext) {
ApplicationContext parentContext = null;
// 聲明:public static final String LOCATOR_FACTORY_SELECTOR_PARAM = "locatorFactorySelector";
String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
// 聲明:public static final String LOCATOR_FACTORY_KEY_PARAM = "parentContextKey";
String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);
if (parentContextKey != null) {
// locatorFactorySelector可能會為null, 則會使用預設的 "classpath*:beanRefContext.xml"
BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isDebugEnabled()) {
logger.debug("Getting parent context definition: using parent context key of '" +
parentContextKey + "' with BeanFactoryLocator");
}
this.parentContextRef = locator.useBeanFactory(parentContextKey);
parentContext = (ApplicationContext) this.parentContextRef.getFactory();
}
return parentContext;
}
這裡通過ServletContext 擷取初始化參數locatorFactorySelector指定的定義父容器的xml檔案的位址,同時擷取初始化參數parentContextKey指定的父容器在前面xml檔案中設定的bean名稱。下面是一個列子。
第一步在classes路徑下建立名為parentBeanRefContext.xml的xml檔案(名稱可以随便取),我這裡的内容如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<!-- 指定容器 -->
<bean id="parentContext" class="org.springframework.context.support.ClassPathXmlApplicationContext">
<constructor-arg>
<list>
<value>parentOfRootContext.xml</value>
</list>
</constructor-arg>
</bean>
</beans>
其中parentOfRootContext.xml檔案為一個普通的spring配置檔案,這裡就不舉例了。
第二步,在web.xml檔案中做如下配置。
<!-- 定義locatorFactorySelector參數 -->
<context-param>
<param-name>locatorFactorySelector</param-name>
<param-value>
classpath:parentBeanRefContext.xml
</param-value>
</context-param>
<!-- 定義parentContextKey參數 -->
<context-param>
<param-name>parentContextKey</param-name>
<param-value>parentContext</param-value>
</context-param>
這樣就向容器中指定了一個父容器。在這裡如果在第一步中建立的xml檔案的名稱為beanRefContext.xml,那麼在web.xml檔案中就不用配置locatorFactorySelector參數。
3. 配置并重新整理spring容器
設定了父容器後,執行ContextLoader的configureAndRefreshWebApplicationContext方法,在容器重新整理前對容器進行初始化配置,代碼如下
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 為容器設定一個有用的ID
// 聲明:public static final String CONTEXT_ID_PARAM = "contextId";
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
} else {
// 生成一個預設ID
if (sc.getMajorVersion() == && sc.getMinorVersion() < ) {
// servlet 2.5以前的版本
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getServletContextName()));
} else {
// servlet 2.5及其以上的版本wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
}
// 把ServletContext儲存到容器中
wac.setServletContext(sc);
// 設定容器要加載的配置檔案所在的路徑
// 通過contextConfigLocation上下文參數指定配置檔案路徑
// 聲明:public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
String initParameter = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (initParameter != null) {
wac.setConfigLocation(initParameter);
}
// 在容器重新整理前,自定義容器。
// 執行使用者通過contextInitializerClasses上下文參數指定的容器初始化器
customizeContext(sc, wac);
// 重新整理容器
wac.refresh();
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 為容器設定一個有用的ID
// 聲明:public static final String CONTEXT_ID_PARAM = "contextId";
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
} else {
// 建立一個預設的id
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// 把ServletContext儲存到容器中
wac.setServletContext(sc);
// 設定容器要加載的配置檔案所在的路徑
// 通過contextConfigLocation上下文參數指定配置檔案路徑
// 聲明:public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// 提前執行容器環境對象的initPropertySources方法,以確定servlet屬性資源可應用于容器重新整理前的任何初始化操作。
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
// 在容器重新整理前,自定義容器。
// 執行使用者通過contextInitializerClasses上下文參數指定的容器初始化器
customizeContext(sc, wac);
// 重新整理容器
wac.refresh();
}
這段代碼處理配置在web.xml中的2個初始化參數,第一個是用于标志容器id的contextId初始化參數,第二是用于指定配置檔案位址的contextConfigLocation初始化參數。對于contextId參數沒有過多的探讨,至于contextConfigLocation參數,可以配置,也可以不配置。如果需要通過contextConfigLocation參數指定多個配置檔案,配置檔案位址之間可以通過英文逗号、分号、空格、制表符、換行符隔開。如果沒有配置contextConfigLocation參數,XmlWebApplicationContext将使用WEB-INF目錄下的預設配置檔案位址,代碼如下。
/** Default config location for the root context */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
/** Default prefix for building a config location for a namespace */
public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";
/** Default suffix for building a config location for a namespace */
public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";
@Override
protected String[] getDefaultConfigLocations() {
if (getNamespace() != null) {
return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
}
else {
// 傳回配置位址/WEB-INF/applicationContext.xml
return new String[] {DEFAULT_CONFIG_LOCATION};
}
}
根據這段代碼,可以獲得兩個資訊。
其一:如果spring容器有命名空間,則開發者可以在WEB-INF目錄下建立以命名空間為名稱的xml配置檔案。
其二:如果spring容器沒有命名空間,則開發者可以在WEB-INF目錄下建立以applicationContext為名稱的xml配置檔案。
在configureAndRefreshWebApplicationContext方法中還調用ContextLoader的customizeContext方法來執行使用者指定ApplicationContextInitializer對象,下面是customizeContext方法的代碼。
protected void customizeContext(ServletContext servletContext, ConfigurableWebApplicationContext applicationContext) {
// 擷取ApplicationContextInitializer類對象清單
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses = determineContextInitializerClasses(servletContext);
if (initializerClasses.size() == ) {
// 沒有指定任何 ApplicationContextInitializers對象,則什麼都不做,直接傳回
return;
}
Class<?> contextClass = applicationContext.getClass();
ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerInstances =
new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>();
for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
// 從initializerClass對象擷取ApplicationContextInitializer的泛型類對象
Class<?> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
// 檢查contextClass是否是initializerContextClass類對象指定的類的實作
Assert.isAssignable(initializerContextClass, contextClass, String.format(
"Could not add context initializer [%s] as its generic parameter [%s] " +
"is not assignable from the type of application context used by this " +
"context loader [%s]: ", initializerClass.getName(), initializerContextClass.getName(),
contextClass.getName()));
initializerInstances.add(BeanUtils.instantiateClass(initializerClass));
}
ConfigurableEnvironment env = applicationContext.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
// 初始化Property資源
((ConfigurableWebEnvironment)env).initPropertySources(servletContext, null);
}
// 一個一個的執行ApplicationContextInitializer對象
Collections.sort(initializerInstances, new AnnotationAwareOrderComparator());
for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : initializerInstances) {
initializer.initialize(applicationContext);
}
}
這段代碼主要是調用ContextLoader對象的determineContextInitializerClasses方法來擷取ApplicationContextInitializer類對象清單,并使用每個ApplicationContextInitializer對象來對spring容器做更多的初始化操作。下面是determineContextInitializerClasses的代碼。
protected List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
determineContextInitializerClasses(ServletContext servletContext) {
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> classes =
new ArrayList<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>();
// 從ServletContext中擷取應用于所有spring web應用容器的ApplicationContextInitializer實作類全名稱
// 聲明有:public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses";
String globalClassNames = servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM);
if (globalClassNames != null) {
for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
classes.add(loadInitializerClass(className));
}
}
// 從ServletContext中擷取專為為此容器指定的ApplicationContextInitializer實作類全名稱
// 聲明:public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses";
String localClassNames = servletContext.getInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM);
if (localClassNames != null) {
for (String className : StringUtils.tokenizeToStringArray(localClassNames, INIT_PARAM_DELIMITERS)) {
classes.add(loadInitializerClass(className));
}
}
return classes;
}
private Class<ApplicationContextInitializer<ConfigurableApplicationContext>> loadInitializerClass(String className) {
try {
Class<?> clazz = ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
Assert.isAssignable(ApplicationContextInitializer.class, clazz);
return (Class<ApplicationContextInitializer<ConfigurableApplicationContext>>) clazz;
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException("Failed to load context initializer class [" + className + "]", ex);
}
}
determineContextInitializerClasses方法從ServletContext中擷取初始化參數contextInitializerClasses和globalInitializerClasses的值,這個值指定了使用者自定義的ApplicationContextInitializer實作類全名稱。然後根據類的全名稱建立Class對象。其中如果需要指定多個ApplicationContextInitializer實作類,那麼實作類的全名稱之間使用英文逗号隔開,比如下面的配置。
<!-- 定義globalInitializerClasses參數 -->
<context-param>
<param-name>globalInitializerClasses</param-name>
<param-value>
com.damuzee.web.app.GlobalApplicationContextInitializer1,
com.damuzee.web.app.GlobalApplicationContextInitializer2,
com.damuzee.web.app.GlobalApplicationContextInitializer3
</param-value>
</context-param>
<!-- 定義contextInitializerClasses參數 -->
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>
com.damuzee.web.app.XmlApplicationContextInitializer1,
com.damuzee.web.app.XmlApplicationContextInitializer2,
com.damuzee.web.app.XmlApplicationContextInitializer3
</param-value>
</context-param>
到此,在web應用中spring容器初始化的第一個階段就完成了。configureAndRefreshWebApplicationContext方法通過調用容器的refresh()方法進入容器初始化的第二階段——容器的重新整理過程
第二階段: 容器的重新整理過程
關于spring容器的重新整理過程已經在另一篇文章中描述了,詳見 Spring ApplicationContext的重新整理過程
總結
在spring容器初始化的第一個階段,我們可以通過web.xml檔案的配置來定制spring容器。通過web.xml檔案,我們可以指定其他容器類、父容器、容器的id、需要加載的配置檔案位址、以及自定義容器初始化器ApplicationContextInitializer對象。具體例子如下。
- 通過設定contextClass參數指定容器,例如
<context-param>
<param-name>contextClass</param-name>
<param-value>
com.chyohn.context.XmlWebApplicationContext
</param-value>
</context-param>
- 設定locatorFactorySelector和parentContextKey參數指定父容器,例如
<!-- 定義locatorFactorySelector參數 -->
<context-param>
<param-name>locatorFactorySelector</param-name>
<param-value>
classpath:parentBeanRefContext.xml
</param-value>
</context-param>
<!-- 定義parentContextKey參數 -->
<context-param>
<param-name>parentContextKey</param-name>
<param-value>parentContext</param-value>
</context-param>
上面的parentBeanRefContext.xml配置如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<!-- 指定容器 -->
<bean id="parentContext" class="org.springframework.context.support.ClassPathXmlApplicationContext">
<constructor-arg>
<list>
<value>parentOfRootContext.xml</value>
</list>
</constructor-arg>
</bean>
</beans>
- 設定contextId參數指定容器的Id,配置如下。
<context-param>
<param-name>contextId</param-name>
<param-value>myContextId</param-value>
</context-param>
- 設定contextConfigLocation參數指定加載的配置檔案位址,配置如下。
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:webApplicationContent.xml
classpath:application-service.xml
</param-value>
</context-param>
- 設定contextInitializerClasses參數指定容器初始化器,該初始化器将在容器重新整理前執行,如果有多個初始化器,使用英文逗号“,”隔開,例如
<!-- 定義contextInitializerClasses參數 -->
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>
com.damuzee.web.app.XmlApplicationContextInitializer1,
com.damuzee.web.app.XmlApplicationContextInitializer2,
com.damuzee.web.app.XmlApplicationContextInitializer3
</param-value>
</context-param>
- 設定globalInitializerClasses參數指定所有容器公共的初始化器,該初始化器将在容器重新整理前執行,如果有多個初始化器,使用英文逗号“,”隔開,例如
<!-- 定義globalInitializerClasses參數 -->
<context-param>
<param-name>globalInitializerClasses</param-name>
<param-value>
com.damuzee.web.app.GlobalApplicationContextInitializer1,
com.damuzee.web.app.GlobalApplicationContextInitializer2,
com.damuzee.web.app.GlobalApplicationContextInitializer3
</param-value>
</context-param>
有了ApplicationContextInitializer對象,可以對容器的初始化做更多操作,比如設定容器id、設定父容器、設定加載的配置檔案位址、添加容器級的bean工廠後處理器、監聽器等等。