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

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

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationgContext.xml");

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

    <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方法,方法的具體代碼如下。

    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }

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

    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,看到后面就會慢慢明白。直接關注重點代碼,代碼如下。

            if (this.context == null) {
                this.context = createWebApplicationContext(servletContext);
            }

這里的context是ContextLoader的一個變量,聲明代碼如下。

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,它的聲明如下。

    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文件,查找并打開這個文件,文件內容如下。

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

這里配置了Spring默認的WebApplicationContext接口實現類XmlWebApplicationContext.java?;氐絚reateWebApplicationContext方法,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();
    }

還是一樣,關注重點代碼??匆幌翪ONFIG_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()); 
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容