【Spring】淺談ContextLoaderListener及其上下文與DispatcherServlet的區(qū)別

一般在使用SpingMVC開發(fā)的項目中带射,一般都會在web.xml文件中配置ContextLoaderListener監(jiān)聽器,如下:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

在開始講解這個之前先講講web工程的上下文循狰,對于一個web容器窟社,web容器提供了一個全局的上下文環(huán)境券勺,這個上下文就是ServletContext,其為后面Spring IOC容器提供宿主環(huán)境灿里。

在web容器啟動時會觸發(fā)容器初始化事件关炼,contextLoaderListener監(jiān)聽到這個事件后其contextInitialized方法就會被調(diào)用,在這個方法中钠四,spring會初始化一個啟動上下文盗扒,這個上下文就是根上下文,也就是WebApplicationContext缀去,實際實現(xiàn)類一般是XmlWebApplicationContext侣灶,這個其實就是spring的IoC容器,這個IoC容器初始化完后缕碎,Spring會將它存儲到ServletContext褥影,可供后面獲取到該IOC容器中的bean。

下面一步步來跟進(jìn)咏雌,看下ContextLoaderListener源碼:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }

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

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

從上面可以看出ContextLoaderListener繼承ContextLoader類并實現(xiàn)了ServletContextListener接口凡怎,ServletContextListener接口中只有初始化和銷毀的兩個方法,如下:

public interface ServletContextListener extends EventListener {
    /**
     ** Notification that the web application initialization
     ** process is starting.
     ** All ServletContextListeners are notified of context
     ** initialization before any filter or servlet in the web
     ** application is initialized.
     */
    public void contextInitialized ( ServletContextEvent sce );

    /**
     ** Notification that the servlet context is about to be shut down.
     ** All servlets and filters have been destroy()ed before any
     ** ServletContextListeners are notified of context
     ** destruction.
     */
    public void contextDestroyed ( ServletContextEvent sce );
}

ContextLoaderListener主要的功能還是在繼承的ContextLoader類中實現(xiàn)赊抖,接下來看看ContextLoaderListener中上下文初始化的方法统倒,也就是:

/**
 * Initialize the root web application context.
 */
@Override
public void contextInitialized(ServletContextEvent event) {
    initWebApplicationContext(event.getServletContext());
}

跟進(jìn)initWebApplicationContext()方法,其調(diào)用的實現(xiàn)就在ContextLoader類中:

/**
 * Initialize Spring's web application context for the given servlet context,
 * using the application context provided at construction time, or creating a new one
 * according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
 * "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
 * @param servletContext current servlet context
 * @return the new WebApplicationContext
 * @see #ContextLoader(WebApplicationContext)
 * @see #CONTEXT_CLASS_PARAM
 * @see #CONFIG_LOCATION_PARAM
 */
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 {
        // Store context in local instance variable, to guarantee that
        // it is available on ServletContext shutdown.
        if (this.context == null) {
            // 創(chuàng)建WebApplicationContext上下文
            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);
                }
                // 對WebApplicationContext進(jìn)行初始化房匆,初始化參數(shù)從web.xml中取
                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);
        }

        /* 省略部分代碼 */
}

上面initWebApplicationContext()方法中,通過createWebApplicationContext(servletContext)創(chuàng)建root上下文(即IOC容器)报亩,之后Spring會以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE屬性為Key浴鸿,將該root上下文存儲到ServletContext中,下面看看createWebApplicationContext(servletContext)源碼:

/**
 * Instantiate the root WebApplicationContext for this loader, either the
 * default context class or a custom context class if specified.
 * <p>This implementation expects custom contexts to implement the
 * {@link ConfigurableWebApplicationContext} interface.
 * Can be overridden in subclasses.
 * <p>In addition, {@link #customizeContext} gets called prior to refreshing the
 * context, allowing subclasses to perform custom modifications to the context.
 * @param sc current servlet context
 * @return the root WebApplicationContext
 * @see ConfigurableWebApplicationContext
 */
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    // 確定載入的上下文的類型弦追,參數(shù)是在web.xml中配置的contextClass(沒有則使用默認(rèn)的)
    Class<?> contextClass = determineContextClass(sc);
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
            "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    // 初始化WebApplicationContext并強(qiáng)轉(zhuǎn)為ConfigurableWebApplicationContext類型
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

從上面源碼也可以看出使用createWebApplicationContext方法創(chuàng)建的上下文肯定是實現(xiàn)了ConfigurableWebApplicationContext接口岳链,否則拋出異常。上面createWebApplicationContext(servletContext)方法里的determineContextClass方法用于查找root上下文的Class類型劲件,看源碼:

/**
 * Return the WebApplicationContext implementation class to use, either the
 * default XmlWebApplicationContext or a custom context class if specified.
 * @param servletContext current servlet context
 * @return the WebApplicationContext implementation class to use
 * @see #CONTEXT_CLASS_PARAM
 * @see org.springframework.web.context.support.XmlWebApplicationContext
 */
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);
        }
    }
}

從以上可以看到如果web.xml中配置了實現(xiàn)ConfigurableWebApplicationContext的contextClass類型就用那個參數(shù)掸哑,否則使用默認(rèn)的XmlWebApplicationContext。

上面ContextLoader類的initWebApplicationContext()方法里還有個加載父上下文的方法loadParentContext(ServletContext servletContext)零远,也來看看其源碼:

/**
 * Template method with default implementation (which may be overridden by a
 * subclass), to load or obtain an ApplicationContext instance which will be
 * used as the parent context of the root WebApplicationContext. If the
 * return value from the method is null, no parent context is set.
 * <p>The main reason to load a parent context here is to allow multiple root
 * web application contexts to all be children of a shared EAR context, or
 * alternately to also share the same parent context that is visible to
 * EJBs. For pure web applications, there is usually no need to worry about
 * having a parent context to the root web application context.
 * <p>The default implementation uses
 * {@link org.springframework.context.access.ContextSingletonBeanFactoryLocator},
 * configured via {@link #LOCATOR_FACTORY_SELECTOR_PARAM} and
 * {@link #LOCATOR_FACTORY_KEY_PARAM}, to load a parent context
 * which will be shared by all other users of ContextsingletonBeanFactoryLocator
 * which also use the same configuration parameters.
 * @param servletContext current servlet context
 * @return the parent application context, or {@code null} if none
 * @see org.springframework.context.access.ContextSingletonBeanFactoryLocator
 */
protected ApplicationContext loadParentContext(ServletContext servletContext) {
    ApplicationContext parentContext = null;
    String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
    String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);

    if (parentContextKey != null) {
        // locatorFactorySelector may be null, indicating the default "classpath*:beanRefContext.xml"
        BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
        Log logger = LogFactory.getLog(ContextLoader.class);
        if (logger.isDebugEnabled()) {
            logger.debug("Getting parent context definition: using parent context key of '" +
                    parentContextKey + "' with BeanFactoryLocator");
        }
        this.parentContextRef = locator.useBeanFactory(parentContextKey);
        parentContext = (ApplicationContext) this.parentContextRef.getFactory();
    }

    return parentContext;
}

上面源碼就是實現(xiàn)根據(jù)locatorFactorySelector和parentContextKey來給上下文設(shè)置父上下文苗分,前提是我們在web.xml中配置了這兩個參數(shù),不過一般開發(fā)中很少會設(shè)置這兩個參數(shù)遍烦,從上面源碼的大段注釋也可以看出如果沒有的話父上下文就為空俭嘁。

在contextLoaderListener監(jiān)聽器初始化完畢后躺枕,開始初始化web.xml中配置的Servlet服猪,這個servlet可以配置多個供填,以DispatcherServlet為例,這個servlet實際上是一個標(biāo)準(zhǔn)的前端控制器罢猪,用以轉(zhuǎn)發(fā)近她、處理每個servlet請求。DispatcherServlet上下文在初始化的時候會建立自己的IoC上下文膳帕,用以持有spring mvc相關(guān)的bean粘捎。在建立DispatcherServlet自己的IoC上下文時,會利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先從ServletContext中獲取之前的根上下文(即WebApplicationContext)作為自己上下文的parent上下文危彩。有了這個parent上下文之后攒磨,再初始化自己持有的上下文。這個DispatcherServlet初始化自己上下文的工作在其initStrategies方法中實現(xiàn)的汤徽,基本工作就是初始化處理器映射娩缰、視圖解析等。這個servlet自己持有的上下文默認(rèn)實現(xiàn)類也是XmlWebApplicationContext谒府。初始化完畢后拼坎,spring以與servlet的名字相關(guān)的屬性為Key,也將其存到ServletContext中完疫。這樣每個servlet就持有自己的上下文泰鸡,即擁有自己獨立的bean空間,同時各個servlet共享相同的bean壳鹤,即根上下文(WebApplicationContext)盛龄。

最后講講ContextLoaderListener與DispatcherServlet所創(chuàng)建的上下文ApplicationContext的區(qū)別:

  1. ContextLoaderListener中創(chuàng)建ApplicationContext主要用于整個Web應(yīng)用程序需要共享的一些組件,比如DAO器虾,數(shù)據(jù)庫的ConnectionFactory等讯嫂。而由DispatcherServlet創(chuàng)建的ApplicationContext主要用于和該Servlet相關(guān)的一些組件,比如Controller兆沙、ViewResovler等欧芽。
  2. 對于作用范圍而言,在DispatcherServlet中可以引用由ContextLoaderListener所創(chuàng)建的ApplicationContext葛圃,而反過來不行千扔。

這兩個ApplicationContext都是通過ServletContext的setAttribute方法放到ServletContext中的。從web.xml的配置可知ContextLoaderListener會先于DispatcherServlet創(chuàng)建ApplicationContext库正,DispatcherServlet在創(chuàng)建ApplicationContext時會先找到由ContextLoaderListener所創(chuàng)建的ApplicationContext曲楚,再將后者的ApplicationContext作為參數(shù)傳給DispatcherServlet的ApplicationContext的setParent()方法,作為它的父上下文褥符,在Spring源代可以看出:

protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
    Class<?> contextClass = getContextClass();
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Servlet with name '" + getServletName() +
                "' will try to create custom WebApplicationContext context of class '" +
                contextClass.getName() + "'" + ", using parent context [" + parent + "]");
    }
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
                "Fatal initialization error in servlet with name '" + getServletName() +
                "': custom WebApplicationContext class [" + contextClass.getName() +
                "] is not of type ConfigurableWebApplicationContext");
    }
    ConfigurableWebApplicationContext wac =
            (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    
    // 設(shè)置父ApplicationContext
    wac.setParent(parent);
    wac.setConfigLocation(getContextConfigLocation());

    configureAndRefreshWebApplicationContext(wac);

    return wac;
}

這里wac即為由DisptcherServlet創(chuàng)建的ApplicationContext龙誊,而parent則為有ContextLoaderListener創(chuàng)建的ApplicationContext。

當(dāng)Spring在執(zhí)行ApplicationContext的getBean時喷楣,如果在自己context中找不到對應(yīng)的bean趟大,則會在父ApplicationContext中去找鹤树。這也解釋了為什么我們可以在DispatcherServlet中獲取到由ContextLoaderListener對應(yīng)的ApplicationContext中的bean。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逊朽,一起剝皮案震驚了整個濱河市罕伯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叽讳,老刑警劉巖追他,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異岛蚤,居然都是意外死亡邑狸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門涤妒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來推溃,“玉大人,你說我怎么就攤上這事届腐√玻” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵犁苏,是天一觀的道長硬萍。 經(jīng)常有香客問我,道長围详,這世上最難降的妖魔是什么朴乖? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮助赞,結(jié)果婚禮上买羞,老公的妹妹穿的比我還像新娘。我一直安慰自己雹食,他們只是感情好畜普,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著群叶,像睡著了一般吃挑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上街立,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天舶衬,我揣著相機(jī)與錄音,去河邊找鬼赎离。 笑死逛犹,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播虽画,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掠手,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了狸捕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤众雷,失蹤者是張志新(化名)和其女友劉穎灸拍,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體砾省,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡鸡岗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了编兄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片轩性。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖狠鸳,靈堂內(nèi)的尸體忽然破棺而出揣苏,到底是詐尸還是另有隱情,我是刑警寧澤件舵,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布卸察,位于F島的核電站,受9級特大地震影響铅祸,放射性物質(zhì)發(fā)生泄漏坑质。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一临梗、第九天 我趴在偏房一處隱蔽的房頂上張望涡扼。 院中可真熱鬧,春花似錦盟庞、人聲如沸吃沪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽巷波。三九已至,卻和暖如春卸伞,著一層夾襖步出監(jiān)牢的瞬間抹镊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工荤傲, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留垮耳,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像终佛,于是被迫代替她去往敵國和親俊嗽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容