一、Spring與WEB容器整合
web項目中,Spring啓動是在web.xml配置監聽器,如下所示:
<!-- 配置Spring上下文監聽器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
可以看看ContextLoaderListener類,它實現了Tomcat容器的ServletContextListener接口,所以它與普通的Servlet監聽是一樣的。同樣是重寫到兩個方法:contextInitialized()方法在web容器初始化時執行,contextDestroyed()方法在容器銷燬時執行。
WEB容器啓動時會觸發初始化事件,ContextLoaderListener監聽到這個事件,其contextInitialized()方法會被調用,在這個方法中Spring會初始化一個根上下文,即WebApplicationContext。這是一個接口,其實際默認實現類是XmlWebApplicationContext。這個就是Spring IOC的容器,其對應bean定義的配置信息由web.xml中的context-param來指定
<!-- 配置Spring配置文件路徑 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:applicationContext.xmlclasspath*:applicationContext-shiro.xml </param-value> </context-param>
在Spring IOC 容器啓動初始化完畢之後,會將其儲存到ServletContext中。形式如下:servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, WebApplicationContext context);
在ContextLoaderListener類中,只是實現了ServletContextListener提供的到兩個方法,Spring啓動主要的邏輯在父類ContextLoader的方法initWebApplicationContext實現。ContextLoaderListener的作用就是啓動web容器時自動裝配ApplicationContext的配置信息。更細化一點講,Spring的啓動過程其實就是Spring IOC容器的啓動過程。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
/**
* Close the root web application context.
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
二、ContextLoader剖析
從上一部分ContextLoaderListener類可以得知,ContextLoader實際執行Spring容器的初始化,Spring整個的配置工作都是在ContextLoader完成的,這裏參數ServletContextEvent由web容器提供,不做說明。ContextLoaderListener很好理解,所以我們主要看ContextLoader類。用Maven引入Spring的源碼,打開ContextLoader類,類註釋的第一行就是
/** * Performs the actual initialization work for the root application context. ...... **/
用google翻譯:實際執行根應用上下文的初始化工作。這裏的根應用上下文就是上文所寫的WebApplicationContext。我們先看看ContextLoader的時序圖
ContextLoader類initWebApplicationContext()方法
/**
* Initialize Spring's web application context for the given servlet context,
* using the application context provided at construction time, or creating a new one
* according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
* "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
* @param servletContext current servlet context
* @return the new WebApplicationContext
* @see #ContextLoader(WebApplicationContext)
* @see #CONTEXT_CLASS_PARAM
* @see #CONFIG_LOCATION_PARAM
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
//判斷ServletContext是否已經存在WebApplication,如果存在則拋出異常
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 {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
//創建WebApplicationContext(下文有說明)
this.context = createWebApplicationContext(servletContext);
}
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) {
// 得到根上下文的父上下文,然後設置到根上下文 。一般的web項目parent爲空
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
//從web.xml加載參數,初始化跟上下文WebApplicationContext,創建bean工廠和bean對象。
//這個過程比較麻煩,下一篇文章專門分析
configureAndRefreshWebApplicationContext(cwac, 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;
}
}
在這個方法中ServletContext是由web容器監聽器(ContextLoaderListener)提供。首先判斷servlectContext中是否已經存在根上下文,如果存在,則拋出異常;否則通過createWebApplicationContext方法創建新的根上下文。然後通過loadParentContext()方法爲其設置父上下文。再通過configureAndRefreshWebApplicationContext爲根上下文構建bean工廠和bean對象。 最後把上下文存入servletContext,並且存入currentContextPerThread。至此初始化過程完畢,接下來可以獲取WebApplicationContext,進而用getBean("bean name")得到bean。
createWebApplicationContext()方法用來創建根上下文:
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
//從web.xml配置的contextClass參數中獲取上下文類名,如果contextClass爲空,則使用默認的。
//下文有說明
Class<?> contextClass = determineContextClass(sc);
//根上下文必須是ConfigurableWebApplicationContext的子類,否則拋出異常
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
//BeanUtils.instantiateClass工具方法,根據類名創建類
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
determineContextClass()方法返回根上下文的類名
protected Class<?> determineContextClass(ServletContext servletContext) {
//從web.xml獲得參數contextClass,在一般的web項目中,此參數爲null
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 {
//獲得根上下文WebApplicationContext的默認實現類的類名,defaultStrategies是Properties類型,
//在CotnextLoader類開頭static語句塊中初始化
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);
}
}
}
WebApplicationContext默認實現類的類名獲取
static {
try {
//獲取當前包下面的ContextLoader.properties文件,文件內容是:
//org.springframework.web.context.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());
}
}