SpringMVC組件 源碼閱讀(IoC容器在Web環境中的啓動)

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自帶的上下文的雙親上下文。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章