【Spring】ContextLoaderListener及其上下文與DispatcherServlet的區別

通常在使用SpingMVC開發的項目中,通常都會在web.xml文件中配置ContextLoaderListener監聽器,以下:前端

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
複製代碼

在開始講解這個以前先講講web工程的上下文,對於一個web容器,web容器提供了一個全局的上下文環境,這個上下文就是ServletContext,其爲後面Spring IOC容器提供宿主環境。java

在web容器啓動時會觸發容器初始化事件,contextLoaderListener監聽到這個事件後其contextInitialized方法就會被調用,在這個方法中,spring會初始化一個啓動上下文,這個上下文就是根上下文,也就是WebApplicationContext,實際實現類通常是XmlWebApplicationContext,這個其實就是spring的IoC容器,這個IoC容器初始化完後,Spring會將它存儲到ServletContext,可供後面獲取到該IOC容器中的bean。mysql

下面一步步來跟進,看下ContextLoaderListener源碼:web

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類並實現了ServletContextListener接口,ServletContextListener接口中只有初始化和銷燬的兩個方法,以下:spring

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類中實現,接下來看看ContextLoaderListener中上下文初始化的方法,也就是:sql

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

跟進initWebApplicationContext()方法,其調用的實現就在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) {
            // 建立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進行初始化,初始化參數從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)建立root上下文(即IOC容器),以後Spring會以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE屬性爲Key,將該root上下文存儲到ServletContext中,下面看看createWebApplicationContext(servletContext)源碼:mvc

/** * 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) {
    // 肯定載入的上下文的類型,參數是在web.xml中配置的contextClass(沒有則使用默認的)
    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並強轉爲ConfigurableWebApplicationContext類型
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
複製代碼

從上面源碼也能夠看出使用createWebApplicationContext方法建立的上下文確定是實現了ConfigurableWebApplicationContext接口,不然拋出異常。上面createWebApplicationContext(servletContext)方法裏的determineContextClass方法用於查找root上下文的Class類型,看源碼:app

/** * 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中配置了實現ConfigurableWebApplicationContext的contextClass類型就用那個參數,不然使用默認的XmlWebApplicationContext。ide

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

上面源碼就是實現根據locatorFactorySelector和parentContextKey來給上下文設置父上下文,前提是咱們在web.xml中配置了這兩個參數,不過通常開發中不多會設置這兩個參數,從上面源碼的大段註釋也能夠看出若是沒有的話父上下文就爲空。

在contextLoaderListener監聽器初始化完畢後,開始初始化web.xml中配置的Servlet,這個servlet能夠配置多個,以DispatcherServlet爲例,這個servlet其實是一個標準的前端控制器,用以轉發、處理每一個servlet請求。DispatcherServlet上下文在初始化的時候會創建本身的IoC上下文,用以持有spring mvc相關的bean。在創建DispatcherServlet本身的IoC上下文時,會利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先從ServletContext中獲取以前的根上下文(即WebApplicationContext)做爲本身上下文的parent上下文。有了這個parent上下文以後,再初始化本身持有的上下文。這個DispatcherServlet初始化本身上下文的工做在其initStrategies方法中實現的,基本工做就是初始化處理器映射、視圖解析等。這個servlet本身持有的上下文默認實現類也是XmlWebApplicationContext。初始化完畢後,spring以與servlet的名字相關的屬性爲Key,也將其存到ServletContext中。這樣每一個servlet就持有本身的上下文,即擁有本身獨立的bean空間,同時各個servlet共享相同的bean,即根上下文(WebApplicationContext)。

最後講講ContextLoaderListener與DispatcherServlet所建立的上下文ApplicationContext的區別:

  1. ContextLoaderListener中建立ApplicationContext主要用於整個Web應用程序須要共享的一些組件,好比DAO,數據庫的ConnectionFactory等。而由DispatcherServlet建立的ApplicationContext主要用於和該Servlet相關的一些組件,好比Controller、ViewResovler等。
  2. 對於做用範圍而言,在DispatcherServlet中能夠引用由ContextLoaderListener所建立的ApplicationContext,而反過來不行。

這兩個ApplicationContext都是經過ServletContext的setAttribute方法放到ServletContext中的。從web.xml的配置可知ContextLoaderListener會先於DispatcherServlet建立ApplicationContext,DispatcherServlet在建立ApplicationContext時會先找到由ContextLoaderListener所建立的ApplicationContext,再將後者的ApplicationContext做爲參數傳給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());

    // 設置父ApplicationContext
    wac.setParent(parent);
    wac.setConfigLocation(getContextConfigLocation());

    configureAndRefreshWebApplicationContext(wac);

    return wac;
}
複製代碼

這裏wac即爲由DisptcherServlet建立的ApplicationContext,而parent則爲有ContextLoaderListener建立的ApplicationContext。

當Spring在執行ApplicationContext的getBean時,若是在本身context中找不到對應的bean,則會在父ApplicationContext中去找。這也解釋了爲何咱們能夠在DispatcherServlet中獲取到由ContextLoaderListener對應的ApplicationContext中的bean。

做者注:按期分享IT互聯網、金融等工做經驗心得、人生感悟,歡迎訂閱交流,目前就任阿里-移動事業部,須要大廠內推的也可到公衆號砸簡歷,或查看我我的資料獲取。(公號ID:weknow619)。

相關文章
相關標籤/搜索