我们已经知道,应用程序上下文是Spring管理的bean所在的容器。但是我们依然要问一个问题:这个上下文是如何创建的?那么在这篇文章中我们来探讨这个问题。
在第一部分中,会说下在 Spring的应用程序上下文中 所谓的 上下文加载器(context loader) 是什么。在第二部分,我们会讨论这个加载器的代码细节。最后一部分,老规矩,写我们自己的一个自定义的loader。在继续之前,需要说一下,loader(加载器) 将根据web application和dispatcher servlet来结合进行分析。其实这也是很多人一碰到源码就像无头苍蝇,不知道从何而起了,刚开始放下所有,从大体去思考该如何入手,这里对设计模式了解就很重要了,还有,源码的类注释很重要,不多说,接着走。
见名知意,上下文加载程序负责构建应用程序上下文。我们可以通过 org.springframework.web.context.ContextLoaderListener的
实例来对其分析( 从我之前的设计模式的文章可以看到,Spring通过观察者模式,其实我自己总结的是电影院模式,声音和画面通过broadcaster发送到listener,listener再调用相应的adapter来处理,所以,这里就直接从listener来找了
),它继承并扩展了同一个包下的 ContextLoader
类。同时还实现了 javax.servlet.ServletContextListener
接口。该接口旨在接收有关servlet上下文中更改变化的通知。只有当它们在( WEB-INF/web.xml
)中注册时,这个接口的实现才能接收这些通知。
在Spring Web应用程序中,会在servlet上下文创建时调用上下文加载程序( context loader
)。之后,开始初始化根Web应用程序上下文( Root WebApplicationContext
)。 Root
非常重要,因为在加载的时候,可以创建两个或更多的上下文。第一个,也是最重要的,定义了整个bean的生存空间,被称为 应用程序上下文(application context)
。另一个是 servlet应用程序上下文
,其包含更多的是面向Web的元素,比如控制器(controllers)或视图解析器。然而我们需要记住的是, servlet
的上下文是根应用程序上下文( Root WebApplicationContext
)的子集,也就是父子容器一说。这意味着 servlet
可以从根应用程序上下文继承所有的bean。这就是为什么你可以在根配置文件中定义一些常见资源(例如:services,这也是我们的Spring xml配置文件为什么要分service和MVC两个的原因),并通过两个不同的servlet进行共享的原因。但是在另一方面,根应用程序上下文不能获取到特定于servlet的bean,看过我的逃逸分析的应该都清楚了吧。
我们可以将注意力拉回到关于上下文加载器的两个作用上:
将根Web应用程序上下文( Root WebApplicationContext
)绑定到调度程序特定的上下文中
自动创建上下文(程序员不需要编写任何东西来使上下文工作)
我们已经了解了上下文加载器的作用。现在,我们来更详细地介绍这其中的细节。web上下文加载器(context loader)类位于 org.springframework.web.context
包中。主类是 ContextLoaderListener
,它扩展了 ContextLoader
类。同时实现了 ServletContextListener
接口。
在上下文创建时调用的方法是 public void contextInitialized(ServletContextEvent event)
。它通过传递给它所接收到的servlet上下文(从事件参数获取 event.getServletContext()
)来调用 ContextLoader
的 initWebApplicationContext
方法。 initWebApplicationContext
方法进行的第一个操作是检查是否有另一个根上下文存在。如果至少存在另一个,则抛出 IllegalStateException
,并且初始化失败。否则,它继续初始化 org.springframework.web.context.WebApplicationContext
实例。如果初始化的实例实现了 ConfigurableWebApplicationContext
接口,则在设置当前应用程序上下文之前,加载器将进行一些设置服务(父上下文,应用程序上下文,servlet上下文等),并通过上下文的 refresh()
方法来准备bean,这已经在关于应用程序上下文的文章中介绍过了。
/** * Initialize the root web application context. */ @Override public void contextInitialized(ServletContextEvent event){ initWebApplicationContext(event.getServletContext()); }
/** * 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 */ publicWebApplicationContextinitWebApplicationContext(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) { this.context = createWebApplicationContext(servletContext); } //此处判断下初始化的实例实现了ConfigurableWebApplicationContext接口 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); } //refresh()准备生米煮熟饭了 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); } if (logger.isDebugEnabled()) { logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); } return this.context; } catch (RuntimeException ex) { logger.error("Context initialization failed", ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } catch (Error err) { logger.error("Context initialization failed", err); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); throw err; } }
ContextLoaderListener
中第二个我们需要关注的方法是 public void contextDestroyed(ServletContextEvent event)
。每当加载程序的上下文关闭时都会调用它。这个方法干了两件事情:
ContextLoader
中的 closeWebApplicationContext()
,它关闭应用程序上下文。通过 ConfigurableWebApplicationContext close()
方法完成上下文关闭。上下文的销毁的过程其实就是销毁bean和关闭bean工厂,此处参考 org.springframework.context.support.AbstractApplicationContext
中的源码,下面相关部分已贴出。 /** * Close the root web application context. */ @Override public void contextDestroyed(ServletContextEvent event){ closeWebApplicationContext(event.getServletContext()); ContextCleanupListener.cleanupAttributes(event.getServletContext()); }
/** * Close Spring's web application context for the given servlet context. If * the default {@link #loadParentContext(ServletContext)} implementation, * which uses ContextSingletonBeanFactoryLocator, has loaded any shared * parent context, release one reference to that shared parent context. * <p>If overriding {@link #loadParentContext(ServletContext)}, you may have * to override this method as well. * @param servletContext the ServletContext that the WebApplicationContext runs in */ public void closeWebApplicationContext(ServletContext servletContext){ servletContext.log("Closing Spring root WebApplicationContext"); try { if (this.context instanceof ConfigurableWebApplicationContext) { ((ConfigurableWebApplicationContext) this.context).close(); } } finally { ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { currentContext = null; } else if (ccl != null) { currentContextPerThread.remove(ccl); } servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); if (this.parentContextRef != null) { this.parentContextRef.release(); } } }
/** * DisposableBean callback for destruction of this instance. * Only called when the ApplicationContext itself is running * as a bean in another BeanFactory or ApplicationContext, * which is rather unusual. * <p>The {@code close} method is the native way to * shut down an ApplicationContext. * @see #close() * @see org.springframework.beans.factory.access.SingletonBeanFactoryLocator */ @Override public void destroy(){ close(); } /** * Close this application context, destroying all beans in its bean factory. * <p>Delegates to {@code doClose()} for the actual closing procedure. * Also removes a JVM shutdown hook, if registered, as it's not needed anymore. * @see #doClose() * @see #registerShutdownHook() */ @Override public void close(){ synchronized (this.startupShutdownMonitor) { doClose(); // If we registered a JVM shutdown hook, we don't need it anymore now: // We've already explicitly closed the context. if (this.shutdownHook != null) { try { Runtime.getRuntime().removeShutdownHook(this.shutdownHook); } catch (IllegalStateException ex) { // ignore - VM is already shutting down } } } } /** * Actually performs context closing: publishes a ContextClosedEvent and * destroys the singletons in the bean factory of this application context. * <p>Called by both {@code close()} and a JVM shutdown hook, if any. * @see org.springframework.context.event.ContextClosedEvent * @see #destroyBeans() * @see #close() * @see #registerShutdownHook() */ protected void doClose(){ if (this.active.get() && this.closed.compareAndSet(false, true)) { if (logger.isInfoEnabled()) { logger.info("Closing " + this); } LiveBeansView.unregisterApplicationContext(this); try { // Publish shutdown event. publishEvent(new ContextClosedEvent(this)); } catch (Throwable ex) { logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex); } // Stop all Lifecycle beans, to avoid delays during individual destruction. try { getLifecycleProcessor().onClose(); } catch (Throwable ex) { logger.warn("Exception thrown from LifecycleProcessor on context close", ex); } // Destroy all cached singletons in the context's BeanFactory. destroyBeans(); // Close the state of this context itself. closeBeanFactory(); // Let subclasses do some final clean-up if they wish... onClose(); this.active.set(false); } } /** * Template method for destroying all beans that this context manages. * The default implementation destroy all cached singletons in this context, * invoking {@code DisposableBean.destroy()} and/or the specified * "destroy-method". * <p>Can be overridden to add context-specific bean destruction steps * right before or right after standard singleton destruction, * while the context's BeanFactory is still active. * @see #getBeanFactory() * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#destroySingletons() */ protected void destroyBeans(){ getBeanFactory().destroySingletons(); } /** * Template method which can be overridden to add context-specific shutdown work. * The default implementation is empty. * <p>Called at the end of {@link #doClose}'s shutdown procedure, after * this context's BeanFactory has been closed. If custom shutdown logic * needs to execute while the BeanFactory is still active, override * the {@link #destroyBeans()} method instead. */ protected void onClose(){ // For subclasses: do nothing by default. }
想象一下,你希望在系统的所有用户之间共享一个信息。你可以用传统的方式做到这一点,也可以使用你定义的上下文加载器。我们通过写一些简单的代码来达到这个目的。还有一个想要实现的功能会涉及多个上下文。我们的应用程序将同时处理 guest
和 connected
两种形式(请同时看下面源码)。可以看到他们的网页的URL匹配规则不一样。使用connected的用户将能够访问与guest规则下以.chtml扩展名结尾的相同的页面,也就是所谓的交集。需要说的是,他们不会共享相同的信息(两个不一样的上下文当然不会一样了)。还不懂的话看下面源码,对于这两者,我们将分别 指定两个servlet上下文。你会看到,因为它,访问connected用户将不会与访问guest共享相同的bean。
我们将从 web.xml
文件开始,请对比上面说的:
<!--?xml version="1.0" encoding="UTF-8"?--> <web-appid="WebApp_ID"version="2.4"xmlns="http://java.sun.com/xml/ns/j2ee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemalocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <servlet> <servlet-name>guest</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/guest-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <--guestisthedefaultservlet--> <servlet-mapping> <servlet-name>guest</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/applicationContext.xml </param-value> </context-param> <--Customizedlistenerwhichwillputsomepersonnalizeddataintoservlet'scontext--> <listener> <listener-class>com.mysite.servlet.CustomizedContextLoader</listener-class> </listener> <servlet> <servlet-name>connected</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/connected-servlet.xml</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>connected</servlet-name> <url-pattern>**.chtml</url-pattern> </servlet-mapping> </web-app>
两个指定的servlet的bean配置文件几乎相同。唯一的区别是connected-servlet.xml包含一个没有与guest servlet共享的bean的定义。这个bean的名字是secretData:
<beanid="secretData"class="com.migo.secret.SecretData"> <propertyname="question"value="How old are you ?"/> <propertyname="answer"value="33"/> </bean> <context:annotation-config/> <context:component-scanbase-package="com.migo"/> <mvc:annotation-driven/>
神秘豆的内容主要由setter和toString方法组成:
public class SecretData{ private String question; private String answer; public void setQuestion(String question){ this.question = question; } public void setAnswer(String answer){ this.answer = answer; } @Override publicStringtoString(){ return "SecretData {question: "+this.question+", answer: "+this.answer+"}"; } }
其他Java代码也很简单。在 CustomizedContextLoader
中,我们重写 contextInitialized
方法来放置共享 servlet
的上下文属性:名字叫 webappVersion
。该属性是一个随机数,用于证明根应用程序上下文的加载程序仅被调用一次:
public class CustomizedContextLoaderextends ContextLoaderListener{ @Override public void contextInitialized(ServletContextEvent event){ System.out.println("[CustomizedContextLoader] Loading context"); // this value could be read from data source, but for the simplicity reasons, we put it statically // number is random because we want to prove that the root context is loaded only once Random random = new Random(); int version = random.nextInt(100001); System.out.println("Version set into servlet's context :"+version); event.getServletContext().setAttribute("webappVersion", version); super.contextInitialized(event); } }
之后,我们传递给用来处理访问网址的 TestController
:
@Controller public class TestController{ @Autowired private ApplicationContext context; @RequestMapping(value = "/test.chtml", method = RequestMethod.GET) publicStringtest(HttpServletRequest request){ LOGGER.debug("[TestController] Webapp version from servlet's context :"+request.getServletContext().getAttribute("webappVersion")); LOGGER.debug("[TestController] Found secretData bean :"+context.getBean("secretData")); return "test"; } @RequestMapping(value = "/test.html", method = RequestMethod.GET) publicStringguestTest(HttpServletRequest request){ LOGGER.debug("[TestController] Webapp version from servlet's context :"+request.getServletContext().getAttribute("webappVersion")); LOGGER.debug("[TestController] Found secretData bean :"+context.getBean("secretData")); return "test"; } }
测试的时候,首先输入 http://localhost:8080/test.chtml,然后输入http://localhost:8080/test.html。然后通过查看日志 :
[CustomizedContextLoader] Loading context Version set into servlet's context :38023 // ... test.chtml [TestController] Webapp version from servlet's context :38023 [TestController] Found secretData bean :SecretData {question: How old are you ?, answer: 33} // ... test.html [TestController] Webapp version from servlet's context :38023 3 avr. 2014 14:01:02 org.apache.catalina.core.StandardWrapperValve invoke GRAVE: Servlet.service() for servlet [guestServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'secretData' is defined] with root cause org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'secretData' is defined at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:638) at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1159) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:282) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:273) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195) at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:973) at com.mysite.controller.TestController.guestTest(TestController.java:114) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:214) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:748) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:689) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:945) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:876) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:931) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:822) at javax.servlet.http.HttpServlet.service(HttpServlet.java:668) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:807) at javax.servlet.http.HttpServlet.service(HttpServlet.java:770) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:304) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:240) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:164) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:462) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:164) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100) at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:562) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:395) at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:250) at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:188) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:302) at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at java.lang.Thread.run(Unknown Source)
首先,将一个信息(“Version set into servlet’s context :”+version)放在servlet上下文中,并由两个servlet上下文继承。第二点是bean的可见性。 Guest
的 servlet
没有看到 secretData bean
,因为它仅在 connected
(connected-servlet.xml)的配置中被定义。
第一部分涉及了这个加载器的两个主要角色:将根Web应用程序上下文( Root WebApplicationContext
)绑定到调度程序特定的上下文中并自动创建上下文。接下来,我们分析了关于上下文加载程序的代码的要点所涉及的细节,如所实现的接口和主要方法的细节实现。最后一部分是我们自定义扩展本地上下文加载器,然后对bean和servlet的属性继承方面进行一些测试。