Spring MVC原理之Spring應用上下文(IoC容器)在Web容器中的啟動分析
Spring IoC是一個獨立的模組,它並不是直接在Web容器中發揮作用的。如果要在Web環境中使用IoC容器,需要Spring為IoC設計一個啟動過程,把IoC容器導人,並在Web容器中建立起來。具體說來,這個啟動過程是和Web容器的啟動過程整合在一起的。在這個過程中,一方面處理Web容器的啟動,另一方面通過設計特定的Web容器攔截器,將IoC容器載入到Web環境中來.並將其初始化。在這個過程建立完成以後, IoC容器才能正常工作,而SpringMVC是建立在IoC容器的基礎上的,這樣才能建立起MVC框架的執行機制,從而響應從Web容器傳遞的HTTP請求。
下面以Tomcat
容器為例子進行分析。在Tomcat中,web.xml
<!-- 載入spring容器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- springmvc的前端控制器 -->
<servlet>
<servlet-name>tt-manager</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class >
<!-- contextConfigLocation不是必須的, 如果不配置contextConfigLocation, springmvc的配置檔案預設在:WEB-INF/servlet的name+"-servlet.xml" -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>tt-manager</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
這裡看到的部署描述是Spring MVC
與Tomcat
的介面部分。在這個部署描述檔案中。首先定義了一個Servlet
物件,它是Spring MVC
的DispatcherServlet
。這個DispatcherServlet
是MVC
中很重要的一個類,起著分發請求的作用。同時,在部署描述中,為這個DispatcherServlet
定義了應的URL
對映,這些URL對映為這個Servlet
指定了需要處理的HTTP
請求。
context-param
引數的配裡用來指定Spring IoC
容器讀取Bean
定義的XML
檔案的路徑,在這裡。這個配且檔案被定義為classpath:spring/applicationContext-*.xml
下的檔案中,可以看到Spring應用的Bean配置。最後,作為Spring MVC的啟動類,Contex tLoaderListener
被定義為一個監聽器,這個監聽器是與Web伺服器的生命週期相關聯的。由ContextLoaderListener
監聽器負責完成IoC容器在Web環魂中的啟動工作.
IoC容器啟動的基本過程
IOC容器的啟動過程就是建立上下文的過程,該上下文是與SerVietContext相伴而生的,同時也是IoC容器在Web應用環境中的具體表現之一。由ConteztLoaderListener
啟動的上下文為根上下文。在根上下文的基礎上.還有一個與Web MVC
相關的上下文用來儲存控制器(DispatcherServlet)需要的MVC物件,作為根上下文的子上下文,構成一個層次化的上下文體系。
在Web容器中啟動Spring應用程式時.首先建立根上下文.然後建立這個上下文體系的,這個上下文體系的建立是由ContextLoder來完成的。具體如下圖:
在web.xml
中,已經配了ContextLoaderListener
,這個ConteztLoaderListener
提Spring
提供的類,是為在Web容器中建立IoC容器服務的。它實現了ServletContextListener
介面。這個介面是在Servlet API中定義的,提供了與Servlet生命週期結合的回撥,比如contextlnitialized
方法和contextDestroyed
方法。
而在Web容器
中,建立WebApplicationContext
的過程,是在contextlnitialized
的介面實現中完成的。具體的載入IoC
容器的過程是由ContextLoaderListener
交由ContextLoader
來完成的,而ContextLoader
本身就是ContextLoaderListener
的基類.它們之間的類關係如下圖:
總之,ContextLoaderListener是SpringMVC的入口,通過父類ContextLoader來實現IoC容器的初始化,通過實現ServletContextListener介面,通過監聽來建立或銷燬WebApplicationContext(IoC容器)。
Web容器中的上下文設計
為了方便在Web環境中使用IoC容器,Spring為Web應用提供了上下文的擴充套件介面WebApplicationContext來滿足啟動過程的需要。
在這個類繼承關係中,可以從熟悉的XmlWebApplicationContext
入手來了解它的介面實現.在介面設計中,最後是通過ApplicationContex
介面與BeanFactory
介面對接的,而對於具體的功能實現,很多都是封裝在其基類AbstractRefreshableWebApplicationContext
中完成的。
WebApplicationContext
和XmlWebApplicationContext
中定義了很多常量,如下:
public interface WebApplicationContext extends ApplicationContext {
//用於在ServletContext中存取 根上下文
String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
String SCOPE_REQUEST = "request";
String SCOPE_SESSION = "session";
String SCOPE_APPLICATION = "application";
String SERVLET_CONTEXT_BEAN_NAME = "servletContext";
String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";
String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";
@Nullable
ServletContext getServletContext(); //可以取得Web容器的ServletContext
}
public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
//預設的Bean資訊路徑
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
//預設的配置檔案位裡在/WEB-INF/目錄下
public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";
//預設的配置檔案字尾名.xml檔案
public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";
public XmlWebApplicationContext() {
}
//refresh()時啟動
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
this.initBeanDefinitionReader(beanDefinitionReader);
this.loadBeanDefinitions(beanDefinitionReader);
}
protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) {
}
//這個初始化過程是由refreshBeanFactory方法來完成的.這裡只是負責載入BeanDefinition
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
String[] configLocations = this.getConfigLocations();
if (configLocations != null) {
String[] var3 = configLocations;
int var4 = configLocations.length;
for(int var5 = 0; var5 < var4; ++var5) {
String configLocation = var3[var5];
reader.loadBeanDefinitions(configLocation);
}
}
}
//這裡是取得Resource位置的地方.使用了設定的預設配置位置!預設的配置位置是/WEB-INF/applicationContext.xml
protected String[] getDefaultConfigLocations() {
return this.getNamespace() != null ? new String[]{"/WEB-INF/" + this.getNamespace() + ".xml"} : new String[]{"/WEB-INF/applicationContext.xml"};
}
}
從程式碼中可以看到,在XmlWebApplicationContext中,基本的上下文功能都已經通過類的繼承獲得,這裡需要處理的是,如何獲取Bean定義資訊,在這裡,就轉化為如何在Web容器環境如這裡指定的/WEB -INF/applicationContext.xml
中獲得Bean定義資訊。這就解釋了為什麼我們開發的時候,web.xml檔案一般放在/WEB -INF/
下。
ContextLoader的設計與實現
對於Spring承載的Web應用而言,可以指定在Web應用程式啟動時載人IoC容器(或者稱為WebAppl icationCon text)。這個功能是由ContextLoaderListener這樣的類來完成的,它是在Web容器中配置的監聽器。這個ContextLoaderListener通過使用ContextLoader來完成實際的WebApplicationContext,也就是IoC容器的初始化工作。這個ContextLoader就像Spring應用程式在Web容器中的啟動器。這個啟動過程是在Web容器中發生的,所以需要根據Web容器部署的要求來定義ContextLoader。
下面分析具體的根上下文的載入過程。在ContextLoaderListener中,實現的是ServletContextListener介面,這個接口裡的函式會結合Web容器的生命週期被呼叫。因為ServletContextListener是ServletContext的監聽者,如果ServletContext發生變化.會觸發出相應的事件,而監聽器一直在對這些事件進行監聽,如果接收到了監聽的事件,就會做出預先設計好的響應動作。
由於ServletContext的變化而觸發的監聽器的響應具體包括:在伺服器啟動時,ServletContext被建立。伺服器關閉時,ServletContext將被銷燬等。對應這些事件及Web容器狀態的變化,在監聽器中定義了對應的事件響應的回撥方法。比如在伺服器啟動時,ServletContextListener的contextlnitialized()
方法被呼叫,伺服器將要關閉時,ServletContextListener的contextDestroyed()
方法被呼叫。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
//在伺服器啟動時,ServletContext被建立
public void contextInitialized(ServletContextEvent event) {
this.initWebApplicationContext(event.getServletContext()); //具體的初始化工作交給ContextLoader來完成
}
//伺服器關閉時,ServletContext將被銷燬
public void contextDestroyed(ServletContextEvent event) {
this.closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
看看ContextLoader中的建立方法:
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
//判斷是否已經有上下文存在,key就是之前WebApplicationContext定義的
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!");
} else {
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) {
//建立根上下文
this.context = this.createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = this.loadParentContext(servletContext);
//這裡載入根上下文的雙親上下文
cwac.setParent(parent);
}
this.configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
//將根上下文儲存到servletContext中,key是之前WebApplicationContext中定義的
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 var8) {
logger.error("Context initialization failed", var8);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8);
throw var8;
} catch (Error var9) {
logger.error("Context initialization failed", var9);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var9);
throw var9;
}
}
}
//建立根上下文的方法
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
//這裡判斷使用什麼樣的類在Web容界中作為IoC容界
Class<?> contextClass = this.determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
} else {
//直接例項化要產生的IOC容器
return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
}
}
在初始化這個上下文以後,該上下文會被儲存到SevletContext中,這樣就建立了一個全域性的關於整個應用的上下文。同時,在啟動Spring MVC時.我們還會看到這個上下文被以後的DispatcherServlet在進行自己持有的上下文的初始化時,設定為DispatcherServlet自帶的上下文的雙親上下文。