通常在使用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的區別:
這兩個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)。