1 Web環境中的SpringMVC
在Web環境中,SpringMVC是建立在IoC容器基礎上的。瞭解SpringMVC,首先要了解Spring的IoC容器是如何在Web環境中被載人並起作用的。
Spring的IoC是一個獨立模塊,它並不直接在Web容器中發揮作用,如果要在Web環境中使用IoC容器,需要Spring爲IoC設計一個啓動過程,把IoC容器導入,並在Web容器中建立起來。具體說來,這個啓動過程是和Web容器的啓動過程集成在一起的。在這個過程中,一方面處理Web容器的啓動,另一方面通過設計特定的Web容器攔截器,將IoC容器載人到Web環境中來,並將其初始化。在這個過程建立完成以後,IoC容器才能正常工作,而SpringMVC是建立在IoC容器的基礎上的,這樣才能建立起MVC框架的運行機制,從而響應從Web容器傳遞的HTTP請求。
下面以Tomcat作爲Web容器的例子進行分析。在Tomcat中,web.xml是應用的部署描述文件。在web.xml中常常經常能看到與Spring相關的部署描述。
<servlet>
<servlet-name>sample</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>6</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>sample</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
web.xml是SpringMVC與Tomcat的接口部分。這個部署描述文件中,首先定義了一個Servlet對象,它是SpringMVC的DispatcherServlet。這個DispatcherServlet是MVC中很重要的一個類,起着分發請求的作用。
同時,在部署描述中,還爲這個DispatcherServlet定義了對應的URL映射,以指定這個Servlet需要處理的HTTP請求範圍。context-param參數用來指定IoC容器讀取Bean的XML文件的路徑,在這裏,這個配置文件被定義爲WEB-INF/applicationContext.xml。其中可以看到Spring應用的Bean配置。
最後,作爲Spring MVC的啓動類,ContextLoaderListener被定義爲一個監聽器,這個監聽器是與Web服務器的生命週期相關聯的,由ContextLoaderListener監聽器負責完成 IoC容器在Web環境中的啓動工作。
DispatchServlet和ContextLoaderListener提供了在Web容器中對Spring的接口,也就是說,這些接口與Web容器耦合是通過ServletContext來實現的(ServletContext是容器和應用溝通的橋樑,從一定程度上講ServletContext就是servlet規範的體現)。這個ServletContext爲Spring的IoC容器提供了一個宿主環境,在宿主環境中,Spring MVC建立起一個IoC容器的體系。這個IoC容器體系是通過ContextLoaderListener的初始化來建立的,在建立IoC容器體系後,把DispatchServlet作爲Spring MVC處理Web請求的轉發器建立起來,從而完成響應HTTP請求的準備。有了這些基本配置,建立在IoC容器基礎上的SpringMVC就可以正常地發揮作用了。下面我們看一下loC容器在Web容器中的啓動代碼實現。
2 IoC容器啓動的基本過程
IoC容器的啓動過程就是建立上下文的過程,該上下文是與ServletContext相伴而生的,同時也是IoC容器在Web應用環境中的具體表現之一。由ContextLoaderListener啓動的上下文爲根上下文。在根上下文的基礎上,還有一個與Web MVC相關的上下文用來保存控制器(DispatcherServlet)需要的MVC對象,作爲根上下文的子上下文,構成一個層次化的上下文體系。在Web容器中啓動Spring應用程序時,首先建立根上下文,然後建立這個上下文體系,這個上下文體系的建立是由ContextLoder來完成的,其UML時序圖如下圖所示。
在web.xml中,已經配置了ContextLoaderListener,它是Spring提供的類,是爲在Web容器中建立IoC容器服務的,它實現了ServletContextListener接口,這個接口是在Servlet API中定義的,提供了與Servlet生命週期結合的回調,比如上下文初始化contextInitialized()方法和上下文銷燬contextDestroyed()方法。而在Web容器中,建立WebApplicationContext的過程,是在contextInitialized()方法中完成的。另外,ContextLoaderListener還繼承了ContextLoader,具體的載入IoC容器的過程是由ContextLoader來完成的。
在ContextLoader中,完成了兩個IoC容器建立的基本過程,一個是在Web容器中建立起雙親IoC容器,另一個是生成相應的WebApplicationContext並將其初始化。
3 Web容器中的上下文設計
先從Web容器中的上下文入手,看看Web環境中的上下文設置有哪些特別之處,然後再
到ContextLoaderListener中去了解整個容器啓動的過程。爲了方便在Web環境中使用IoC容器,
Spring爲Web應用提供了上下文的擴展接口WebApplicationContext來滿足啓動過程的需要,其繼承關係如下圖所示。
在這個類繼承關係中,可以從熟悉的XmlWebApplicationContext入手來了解它的接口實現。在接口設計中,最後是通過ApplicationContex接口與BeanFactory接口對接的,而對於具體的功能實現,很多都是封裝在其基類AbstractRefreshableWebApplicationContext中完成的。
同樣,在源代碼中,也可以分析出類似的繼承關係,在WebApplicationContext中可以看到相關的常量設計,比如ROOT_ WEB_ APPLICATION_CONTEXT_ATTRIBUTE等,這個常量是用來索引在ServletContext中存儲的根上下文的。這個接口類定義的接口方法比較簡單,在這個接口中,定義了一
個getServletContext方法,通過這個方法可以得到當前Web容器的Servlet上下文環境,通過
這個方法,相當於提供了一個Web容器級別的全局環境。
public interface WebApplicationContext extends ApplicationContext {
/**
* 該常量用於在ServletContext中存取根上下文
*/
String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
/**
* 對於WebApplicationContext來說,需要得到Web容器的ServletContext
*/
ServletContext getServletContext();
}
在啓動過程中,Spring會使用一個默認的WebApplicationContext實現作爲IoC容器,這個默認使用的IoC容器就是XmlWebApplicationContext,它繼承了ApplicationContext,在ApplicationContext的基礎上,增加了對Web環境和XML配置定義的處理。在XmlWebApplicationContext的初始化過程中,Web容器中的IoC容器被建立起來,從而在Web容器中建立起整個Spring應用。與前面博文中分析的IoC容器的初始化一樣,這個過程也有loadBeanDefinition對BeanDefinition的載入。在Web環境中,對定位BeanDefinition的Resource有特別的要求,這個要求的處理體現在對getDefaultConfigLocations方法的處理中。這裏使用了默認的BeanDefinition的配置路徑,這個路徑在XmlWebApplicationContext中作爲一個常量定義好了,即/WEB-INF/applicationContext.xml。
public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
/** 若不指定其它文件,spring默認從"/WEB-INF/applicationContext.xml"目錄文件 初始化IoC容器 */
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";
/**
* 此加載過程在 容器refresh()時啓動
*/
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 使用XmlBeanDefinitionReader對指定的BeanFactory進行解析
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// 初始化beanDefinitionReader的屬性,其中,設置ResourceLoader是因爲
// XmlBeanDefinitionReader是DefaultResource的子類,所有這裏同樣會使用
// DefaultResourceLoader來定位BeanDefinition
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
// 該方法是一個空實現
initBeanDefinitionReader(beanDefinitionReader);
// 使用初始化完成的beanDefinitionReader來加載BeanDefinitions
loadBeanDefinitions(beanDefinitionReader);
}
protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) {
}
/**
* 獲取所有的配置文件,然後一個一個載入BeanDefinition
*/
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
String[] configLocations = getConfigLocations();
if (configLocations != null) {
for (String configLocation : configLocations) {
reader.loadBeanDefinitions(configLocation);
}
}
}
/**
* 獲取默認路徑"/WEB-INF/***.xml"下的配置文件,
* 或者獲取"/WEB-INF/applicationContext.xml"配置文件
*/
@Override
protected String[] getDefaultConfigLocations() {
if (getNamespace() != null) {
return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
}
else {
return new String[] {DEFAULT_CONFIG_LOCATION};
}
}
}
從上面的代碼中可以看到,在XmlWebApplicationContext中,基本的上下文功能都已經通過類的繼承獲得,這裏需要處理的是,如何獲取Bean定義信息,在這裏,就轉化爲如何在Web容器環境中獲得Bean定義信息。在獲得Bean定義信息之後,後面的過程基本上就和前面分析的XmlFileSystemBeanFactory一樣,是通過XmlBeanDefinitionReader來載人Bean定義信息的,最終完成整個上下文的初始化過程。
4 ContextLoader的設計與實現
對於Spring承載的Web應用而言,可以指定在Web應用程序啓動時載入IoC容器(或者稱爲WebApplicationContext)。這個功能是由ContextLoaderListener來完成的,它是在Web容器中配置的監聽器,會監聽Web容器的啓動,然後載入IoC容器。這個ContextLoaderListener通過使用ContextLoader來完成實際的WebApplicationContext,也就是IoC容器的初始化工作。這個ContextLoader就像Spring應用程序在Web容器中的啓動器。這個啓動過程是在Web容器中發生的,所以需要根據Web容器部署的要求來定義ContextLoader,相關的配置在概述中已經看到了,這裏就不重複了。
爲了瞭解IoC容器在Web容器中的啓動原理,這裏對啓動器ContextLoaderListener的實現進行分析。這個監聽器是啓動根IoC容器並把它載入到Web容器的主要功能模塊,也是整個Spring Web應用加載IoC的第一個地方。從加載過程可以看到,首先從Servlet事件中得到ServletContext,然後可以讀取配置在web.xml中的各個相關的屬性值,接着ContextLoader會實例化WebApplicationContext,並完成其載人和初始化過程。這個被初始化的第一個上下文,作爲根上下文而存在,這個根上下文載入後,被綁定到Web應用程序的ServletContext上。任何需要訪問根上下文的應用程序代碼都可以從WebApplicationContextUtils類的靜態方法中得到。
下面分析具體的根上下文的載人過程。在ContextLoaderListener中,實現的是ServletContextListener接口,這個接口裏的函數會結合Web容器的生命週期被調用。因爲ServletContextListener是ServletContext的監聽者,如果ServletContext發生變化,會觸發出相應的事件,而監聽器一直在對這些事件進行監聽,如果接收到了監聽的事件,就會做出預先設計好的響應動作。由於ServletContext的變化而觸發的監聽器的響應具體包括:在服務器啓動時,ServletContext被創建的時候,服務器關閉時,ServletContext將被銷燬的時候等。對應這些事件及Web容器狀態的變化,在監聽器中定義了對應的事件響應的回調方法。比如,在服務器啓動時,ServletContextListener的contextInitialized()方法被調用,服務器將要關閉時,ServletContextListener的contextDestroyed()方法被調用。瞭解了Web容器中監聽器的工作原理,下面看看服務器啓動時 ContextLoaderListener的調用完成了什麼。在這個初始化回調中,創建了ContextLoader,同時會利用創建出來的ContextLoader來完成IoC容器的初始化。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
private ContextLoader contextLoader;
/**
* 啓動web應用的 根上下文
*/
public void contextInitialized(ServletContextEvent event) {
// 由於本類直接繼承了ContextLoader,所以能直接使用ContextLoader來初始化IoC容器
this.contextLoader = createContextLoader();
if (this.contextLoader == null) {
this.contextLoader = this;
}
// 具體的初始化工作交給ContextLoader完成
this.contextLoader.initWebApplicationContext(event.getServletContext());
}
}
public class ContextLoader {
public static final String CONTEXT_CLASS_PARAM = "contextClass";
public static final String CONTEXT_ID_PARAM = "contextId";
public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses";
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
public static final String LOCATOR_FACTORY_SELECTOR_PARAM = "locatorFactorySelector";
public static final String LOCATOR_FACTORY_KEY_PARAM = "parentContextKey";
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.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());
}
}
/**
* 由ContextLoader完成根上下文在Web容器中的創建。這個根上下文是作爲Web容器中唯一的實例而存在的,
* 根上下文創建成功後 會被存到Web容器的ServletContext中,供需要時使用。存取這個根上下文的路徑是由
* Spring預先設置好的,在WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE中進行了定義
*/
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) {
// 這裏創建在ServletContext中存儲的根上下文
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
// 載入根上下文的 雙親上下文
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置 並且初始化IoC容器,看到Refresh應該能想到AbstractApplicationContext
// 中的refresh()方法,猜到它是前面介紹的IoC容器的初始化入口
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 將上面創建的WebApplicationContext實例 存到ServletContext中,注意同時被存入的常量
// ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,以後的應用都會根據這個屬性獲取根上下文
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;
}
}
/**
* 創建WebApplicationContext的實例化對象
*/
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 判斷使用什麼樣的類在Web容器中作爲IoC容器
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
// 直接實例化需要產生的IoC容器
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
/**
* 在確定使用何種IoC容器的過程中可以看到,應用可以在部署描述符中指定使用什麼樣的IoC容器,
* 這個指定操作是通過CONTEXT_ CLASS_ PARAM參數的設置完成的。如果沒有指定特定的IoC容器,
* 將使用默認的IoC容器,也就是XmlWebApplicationContext對象作爲在Web環境中使用的IoC容器。
*/
protected Class<?> determineContextClass(ServletContext servletContext) {
// 獲取servletContext中對CONTEXT_CLASS_PARAM(contextClass)參數的配置
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
// 獲取配置的contextClassName對應的clazz對象
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
else {
// 如果沒有配置CONTEXT_CLASS_PARAM,則使用默認的ContextClass
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);
}
}
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
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
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// Generate default id...
if (sc.getMajorVersion() == 2 && sc.getMinorVersion() < 5) {
// Servlet <= 2.4: resort to name specified in web.xml, if any.
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getServletContextName()));
}
else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
}
// 設置ServletContext 及配置文件的位置參數
wac.setServletContext(sc);
String initParameter = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (initParameter != null) {
wac.setConfigLocation(initParameter);
}
customizeContext(sc, wac);
// IoC容器初始化的入口,想不起來的把前面IoC容器初始化的博文再讀10遍
wac.refresh();
}
}
這就是IoC容器在Web容器中的啓動過程,與 應用中啓動IoC容器的方式相類似,所不同的是這裏需要考慮Web容器的環境特點,比如各種參數的設置,IoC容器與Web容器ServletContext的結合等。在初始化這個上下文以後,該上下文會被存儲到SevletContext中,這樣就建立了一個全局的關於整個應用的上下文。同時,在啓動Spring MVC時,我們還會看到這個上下文被以後的DispatcherServlet在進行自己持有的上下文的初始化時,設置爲DispatcherServlet自帶的上下文的雙親上下文。