[Spring框架] Spring中的 ContextLoaderListener 实现原理.
前言: 这是关于Spring的第三篇文章, 打算后续还会写入AOP 和Spring 事务管理相关的文章, 这么好的两个周末 都在看code了, 确实是有所收获, 现在就来记录一下.
在上一篇讲解Spring IOC的文章中, 每次产生ApplicationContext工厂的方式是:
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
这样产生applicationContext 就有一个弊端, 每次访问加载bean 的时候都会产生这个工厂, 所以 这里需要解决这个问题.
解决问题的方法很简单, 在web 启动的时候将applicationContext转到到servletContext中, 因为在web 应用中的所有servlet都共享一个servletContext对象. 那么我们就可以利用ServletContextListener去监听servletContext事件, 当web 应用启动的是时候, 我们就将applicationContext 装载到servletContext中.
然而Spring容器底层已经为我们想到了这一点, 在spring-web-xxx-release.jar包中有一个 已经实现了ServletContextListener的类, 下面我们就来看一下这个类:
1 public class ContextLoaderListener extends ContextLoader implements ServletContextListener { 2 3 private ContextLoader contextLoader; 4 5 6 /** 7 * Create a new {@code ContextLoaderListener} that will create a web application 8 * context based on the "contextClass" and "contextConfigLocation" servlet 9 * context-params. See {@link ContextLoader} superclass documentation for details on 10 * default values for each. 11 * <p>This constructor is typically used when declaring {@code ContextLoaderListener} 12 * as a {@code <listener>} within {@code web.xml}, where a no-arg constructor is 13 * required. 14 * <p>The created application context will be registered into the ServletContext under 15 * the attribute name {@link WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE} 16 * and the Spring application context will be closed when the {@link #contextDestroyed} 17 * lifecycle method is invoked on this listener. 18 * @see ContextLoader 19 * @see #ContextLoaderListener(WebApplicationContext) 20 * @see #contextInitialized(ServletContextEvent) 21 * @see #contextDestroyed(ServletContextEvent) 22 */ 23 public ContextLoaderListener() { 24 } 25 26 /** 27 * Create a new {@code ContextLoaderListener} with the given application context. This 28 * constructor is useful in Servlet 3.0+ environments where instance-based 29 * registration of listeners is possible through the {@link javax.servlet.ServletContext#addListener} 30 * API. 31 * <p>The context may or may not yet be {@linkplain 32 * org.springframework.context.ConfigurableApplicationContext#refresh() refreshed}. If it 33 * (a) is an implementation of {@link ConfigurableWebApplicationContext} and 34 * (b) has <strong>not</strong> already been refreshed (the recommended approach), 35 * then the following will occur: 36 * <ul> 37 * <li>If the given context has not already been assigned an {@linkplain 38 * org.springframework.context.ConfigurableApplicationContext#setId id}, one will be assigned to it</li> 39 * <li>{@code ServletContext} and {@code ServletConfig} objects will be delegated to 40 * the application context</li> 41 * <li>{@link #customizeContext} will be called</li> 42 * <li>Any {@link org.springframework.context.ApplicationContextInitializer ApplicationContextInitializer}s 43 * specified through the "contextInitializerClasses" init-param will be applied.</li> 44 * <li>{@link org.springframework.context.ConfigurableApplicationContext#refresh refresh()} will be called</li> 45 * </ul> 46 * If the context has already been refreshed or does not implement 47 * {@code ConfigurableWebApplicationContext}, none of the above will occur under the 48 * assumption that the user has performed these actions (or not) per his or her 49 * specific needs. 50 * <p>See {@link org.springframework.web.WebApplicationInitializer} for usage examples. 51 * <p>In any case, the given application context will be registered into the 52 * ServletContext under the attribute name {@link 53 * WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE} and the Spring 54 * application context will be closed when the {@link #contextDestroyed} lifecycle 55 * method is invoked on this listener. 56 * @param context the application context to manage 57 * @see #contextInitialized(ServletContextEvent) 58 * @see #contextDestroyed(ServletContextEvent) 59 */ 60 public ContextLoaderListener(WebApplicationContext context) { 61 super(context); 62 } 63 64 /** 65 * Initialize the root web application context. 66 */ 67 public void contextInitialized(ServletContextEvent event) { 68 this.contextLoader = createContextLoader(); 69 if (this.contextLoader == null) { 70 this.contextLoader = this; 71 } 72 this.contextLoader.initWebApplicationContext(event.getServletContext()); 73 } 74 75 /** 76 * Create the ContextLoader to use. Can be overridden in subclasses. 77 * @return the new ContextLoader 78 * @deprecated in favor of simply subclassing ContextLoaderListener itself 79 * (which extends ContextLoader, as of Spring 3.0) 80 */ 81 @Deprecated 82 protected ContextLoader createContextLoader() { 83 return null; 84 } 85 86 /** 87 * Return the ContextLoader used by this listener. 88 * @return the current ContextLoader 89 * @deprecated in favor of simply subclassing ContextLoaderListener itself 90 * (which extends ContextLoader, as of Spring 3.0) 91 */ 92 @Deprecated 93 public ContextLoader getContextLoader() { 94 return this.contextLoader; 95 } 96 97 98 /** 99 * Close the root web application context. 100 */ 101 public void contextDestroyed(ServletContextEvent event) { 102 if (this.contextLoader != null) { 103 this.contextLoader.closeWebApplicationContext(event.getServletContext()); 104 } 105 ContextCleanupListener.cleanupAttributes(event.getServletContext()); 106 } 107 108 }
这里就监听到了servletContext的创建过程, 那么 这个类又是如何将applicationContext装入到serveletContext容器中的呢?
我们接着来看看 :this.contextLoader.initWebApplicationContext(event.getServletContext()) 方法:
1 public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { 2 if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { 3 throw new IllegalStateException( 4 "Cannot initialize context because there is already a root application context present - " + 5 "check whether you have multiple ContextLoader* definitions in your web.xml!"); 6 } 7 8 Log logger = LogFactory.getLog(ContextLoader.class); 9 servletContext.log("Initializing Spring root WebApplicationContext"); 10 if (logger.isInfoEnabled()) { 11 logger.info("Root WebApplicationContext: initialization started"); 12 } 13 long startTime = System.currentTimeMillis(); 14 15 try { 16 // Store context in local instance variable, to guarantee that 17 // it is available on ServletContext shutdown. 18 if (this.context == null) { 19 this.context = createWebApplicationContext(servletContext); 20 } 21 if (this.context instanceof ConfigurableWebApplicationContext) { 22 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; 23 if (!cwac.isActive()) { 24 // The context has not yet been refreshed -> provide services such as 25 // setting the parent context, setting the application context id, etc 26 if (cwac.getParent() == null) { 27 // The context instance was injected without an explicit parent -> 28 // determine parent for root web application context, if any. 29 ApplicationContext parent = loadParentContext(servletContext); 30 cwac.setParent(parent); 31 } 32 configureAndRefreshWebApplicationContext(cwac, servletContext); 33 } 34 } 35 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); 36 37 ClassLoader ccl = Thread.currentThread().getContextClassLoader(); 38 if (ccl == ContextLoader.class.getClassLoader()) { 39 currentContext = this.context; 40 } 41 else if (ccl != null) { 42 currentContextPerThread.put(ccl, this.context); 43 } 44 45 if (logger.isDebugEnabled()) { 46 logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + 47 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); 48 } 49 if (logger.isInfoEnabled()) { 50 long elapsedTime = System.currentTimeMillis() - startTime; 51 logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); 52 } 53 54 return this.context; 55 } 56 catch (RuntimeException ex) { 57 logger.error("Context initialization failed", ex); 58 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); 59 throw ex; 60 } 61 catch (Error err) { 62 logger.error("Context initialization failed", err); 63 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); 64 throw err; 65 } 66 }
这里的重点是:servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
用key:WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE value: this.context的形式将applicationContext装载到servletContext中了.
另外从上面的一些注释我们可以看出: WEB-INF/applicationContext.xml, 如果我们项目中的配置文件不是这么一个路径的话 那么我们使用ContextLoaderListener 就会 出问题, 所以我们还需要在web.xml中配置我们的applicationContext.xml配置文件的路径.
1 <listener> 2 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 3 </listener> 4 5 <context-param> 6 <param-name>contextConfigLocation</param-name> 7 <param-value>classpath:applicationContext.xml</param-value> 8 </context-param>
剩下的就是在项目中开始使用 servletContext中装载的applicationContext对象了:
那么这里又有一个问题, 装载时的key 是 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, 我们在代码中真的要使用这个吗? 其实Spring为我们提供了一个工具类WebApplicationContextUtils, 接着我们先看下如何使用, 然后再去看下这个工具类的源码:
WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
接着来看下这个工具了的源码:
1 /** 2 * Find the root WebApplicationContext for this web application, which is 3 * typically loaded via {@link org.springframework.web.context.ContextLoaderListener}. 4 * <p>Will rethrow an exception that happened on root context startup, 5 * to differentiate between a failed context startup and no context at all. 6 * @param sc ServletContext to find the web application context for 7 * @return the root WebApplicationContext for this web app, or {@code null} if none 8 * @see org.springframework.web.context.WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 9 */ 10 public static WebApplicationContext getWebApplicationContext(ServletContext sc) { 11 return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); 12 }
1 /** 2 * Find a custom WebApplicationContext for this web application. 3 * @param sc ServletContext to find the web application context for 4 * @param attrName the name of the ServletContext attribute to look for 5 * @return the desired WebApplicationContext for this web app, or {@code null} if none 6 */ 7 public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) { 8 Assert.notNull(sc, "ServletContext must not be null"); 9 Object attr = sc.getAttribute(attrName); 10 if (attr == null) { 11 return null; 12 } 13 if (attr instanceof RuntimeException) { 14 throw (RuntimeException) attr; 15 } 16 if (attr instanceof Error) { 17 throw (Error) attr; 18 } 19 if (attr instanceof Exception) { 20 throw new IllegalStateException((Exception) attr); 21 } 22 if (!(attr instanceof WebApplicationContext)) { 23 throw new IllegalStateException("Context attribute is not of type WebApplicationContext: " + attr); 24 } 25 return (WebApplicationContext) attr; 26 }
这里就能很直观清晰地看到 通过key值直接获取到装载到servletContext中的 applicationContext对象了.