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

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

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

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

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

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

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

public WebApplicationContext initWebApplicationContext(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) {
                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);
                    }
                    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,看到後面就會慢慢明白。直接關注重點代碼,代碼以下。app

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

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

private WebApplicationContext context;複製代碼

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

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方法就是用於尋找實現類,若是開發人員在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 {
            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,它的聲明以下。post

private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
    private static final Properties defaultStrategies;
    static {
        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文件,查找並打開這個文件,文件內容以下。ui

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()) {
                    if (cwac.getParent() == null) {
                        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())) {
            String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
            if (idParam != null) {
                wac.setId(idParam);
            }else {
                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);
        }
        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.getWebApplicationCont複製代碼
相關文章
相關標籤/搜索