通常在使用SpingMVC開發的項目中,通常都會在web.xml文件中配置ContextLoaderListener監聽器,以下:html
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
在開始講解這個以前先講講web工程的上下文,對於一個web容器,web容器提供了一個全局的上下文環境,這個上下文就是ServletContext,其爲後面Spring IOC容器提供宿主環境。前端
在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的區別:
- ContextLoaderListener中建立ApplicationContext主要用於整個Web應用程序須要共享的一些組件,好比DAO,數據庫的ConnectionFactory等。而由DispatcherServlet建立的ApplicationContext主要用於和該Servlet相關的一些組件,好比Controller、ViewResovler等。
- 對於做用範圍而言,在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。
父子兩個WebApplicationContext帶來的麻煩
父WebApplicationContext裏的bean能夠在不一樣的子WebApplicationContext裏共享,而不一樣的子WebApplicationContext裏的bean區不干擾。可是實際上有會很多的問題:若是開發者不知道Spring mvc裏分有兩個WebApplicationContext,致使各類重複構造bean,各類bean沒法注入的問題。有一些bean,好比全局的aop處理的類,若是先父WebApplicationContext裏初始化了,那麼子WebApplicationContext裏的初始化的bean就沒有處理到。若是在子WebApplicationContext裏初始化,在父WebApplicationContext裏的類就沒有辦法注入了。