深刻理解Spring系列之七:web應用自動裝配Spring配置

轉載 https://mp.weixin.qq.com/s/Lf4akWFmcyn9ZVGUYNi0Lwjava

在《深刻理解Spring系列之一:開篇》的示例代碼中使用以下方式去加載Spring的配置文件並初始化容器。web

ApplicationContext applicationContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
複製代碼

在web應用中,配置文件都是自動加載的,示例代碼中的方式就不能知足需求了。在web應用中使用Spring,須要在web.xml中添加以下配置spring

<listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:applicationContext.xml;
        </param-value>
    </context-param>
複製代碼

先了解一下ServletContext和ServletContextListener。ServletContext定義了一些方法方便Servlet和Servlet容器進行通信,在一個web應用中全部的Servlet都公用一個ServletContext,Spring在和web應用結合使用的時候,是將Spring的容器存到ServletContext中的,通俗的說就是將一個ApplicationContext存儲到ServletContext的一個Map屬性中bash

而ServletContextListener用於監聽ServletContext一些事件。分析就從ContextLoaderListener開始。在web應用啓動讀取web.xml時,發現配置了ContextLoaderListener,而ContextLoaderListener實現了ServletContextListener接口,所以會執行ContextLoaderListener類中的contextInitialized方法,方法的具體代碼以下。app

@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}
複製代碼

繼續進入initWebApplicationContext方法,這個方法在其父類ContextLoader實現,根據方法名能夠看出這個方法是用於初始化一個WebApplicationContext,簡單理解就是初始化一個Web應用下的Spring容器。方法的具體代碼以下。ide

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
            // 檢查servletContext是否已經存儲了一個默認名稱的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!");
		}

		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) {
				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) {
						// The context instance was injected without an explicit parent ->
						// determine parent for root web application context, if any.
						ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}
					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是否已經存儲了一個默認名稱的WebApplicationContext,由於在一個應用中Spring容器只能有一個,因此須要校驗,至於這個默認的名稱爲何是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,這個變量聲明代碼以下:函數

String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
複製代碼

看到後面就會慢慢明白。直接關注重點代碼,代碼以下。工具

if (this.context == null) {
				this.context = createWebApplicationContext(servletContext);
			}
複製代碼

這裏的context是ContextLoader的一個變量,聲明代碼以下。post

private WebApplicationContext context;
複製代碼

繼續進入createWebApplicationContext方法,具體代碼以下。ui

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);
	}
複製代碼

這個方法主要用於建立一個WebApplicationContext對象。由於WebApplicationContext只是一個接口,不能建立對象,因此須要找到一個WebApplicationContext接口的實現類,determineContextClass方法就是用於尋找實現類

WebApplicationContext的實現類

若是開發人員在web.xml中配置了一個參數名爲contextClass,值爲WebApplicationContext接口實現類,那就會返回這個配置的實現類Class;若是沒有配置,則會返回Spring默認的實現類XmlWebApplicationContext。直接進入determineContextClass方法體,代碼以下。

protected Class<?> determineContextClass(ServletContext servletContext) {
		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 {
		    // 用於獲取Spring默認的WebApplicationContext接口實現類XmlWebApplicationContext.java
			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);
			}
		}
	}
複製代碼

上面的代碼中使用了defaultStrategies,用於獲取Spring默認的WebApplicationContext接口實現類XmlWebApplicationContext.java,

defaultStrategies 聲明以下。

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.java的路徑下,有一個ContextLoader.properties文件,查找並打開這個文件,文件內容以下。

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
複製代碼

這裏配置了Spring默認的WebApplicationContext接口實現類XmlWebApplicationContext.java

回到createWebApplicationContext方法,WebApplicationContext接口實現類的Class已經找到,而後就是使用構造函數進行初始化完成WebApplicationContext對象建立。

繼續回到initWebApplicationContext方法,此時這個context就指向了剛剛建立的WebApplicationContext對象。由於XmlWebApplicationContext間接實現了ConfigurableWebApplicationContext接口,因此將會執行以下代碼。

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) {
						// The context instance was injected without an explicit parent ->
						// determine parent for root web application context, if any.
						ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}
					configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}

複製代碼

這裏關注重點代碼configureAndRefreshWebApplicationContext(cwac,servletContext),configureAndRefreshWebApplicationContext方法具體代碼以下。

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...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(sc.getContextPath()));
			}
		}

		wac.setServletContext(sc);
		//關注重點代碼
		String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}

		// The wac environment's #initPropertySources will be called in any case when the context // is refreshed; do it eagerly here to ensure servlet property sources are in place for // use in any post-processing or initialization that occurs below prior to #refresh ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); } customizeContext(sc, wac); wac.refresh(); } 複製代碼

仍是同樣,關注重點代碼。看一下CONFIG_LOCATION_PARAM這個常量的值是"contextConfigLocation",OK,這個就是web.xml中配置applicationContext.xml的。這個參數若是沒有配置,在XmlWebApplicationContext中是有默認值的,具體的值以下。

public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
複製代碼

也就是說,若是沒有配置contextConfigLocation參數,將會使用/WEB-INF/applicationContext.xml。關注configureAndRefreshWebApplicationContext方法的最後一行代碼wac.refresh(),是否是有點眼熟,繼續跟蹤代碼,refresh方法的具體代碼以下。

public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            prepareRefresh();
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
            prepareBeanFactory(beanFactory);
            try {
                postProcessBeanFactory(beanFactory);
                invokeBeanFactoryPostProcessors(beanFactory);
                registerBeanPostProcessors(beanFactory);
                initMessageSource();
                initApplicationEventMulticaster();
                onRefresh();
                registerListeners();
                finishBeanFactoryInitialization(beanFactory);
                finishRefresh();
            }catch (BeansException ex) {
                logger.warn("Exception encountered during context initialization - cancelling refresh attempt", ex);
                destroyBeans();
                cancelRefresh(ex);
                throw ex;
            }
        }
    }
複製代碼

這個refresh方法就是《深刻理解Spring系列之四:BeanDefinition裝載前奏曲》介紹的那個refresh方法,用於完成Bean的解析、實例化及註冊。 繼續分析,回到 initWebApplicationContext 方法,將執行以下代碼。

servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
複製代碼

能夠看到,這裏將初始化後的context存到了servletContext中,具體的就是存到了一個Map變量中,key值就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE這個常量

使用Spring的 WebApplicationContextUtils 工具類獲取這個 WebApplicationContext 方式以下。

WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
複製代碼
相關文章
相關標籤/搜索