• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

Spring 5 源码解析(9):Spring 中的 Context loader

Spring winrains 来源:一叶知秋 1年前 (2019-11-03) 59次浏览

我们已经知道,应用程序上下文是Spring管理的bean所在的容器。但是我们依然要问一个问题:这个上下文是如何创建的?那么在这篇文章中我们来探讨这个问题。
在第一部分中,会说下在Spring的应用程序上下文中所谓的上下文加载器(context loader)是什么。在第二部分,我们会讨论这个加载器的代码细节。最后一部分,老规矩,写我们自己的一个自定义的loader。在继续之前,需要说一下,loader(加载器) 将根据web application和dispatcher servlet来结合进行分析。其实这也是很多人一碰到源码就像无头苍蝇,不知道从何而起了,刚开始放下所有,从大体去思考该如何入手,这里对设计模式了解就很重要了,还有,源码的类注释很重要,不多说,接着走。

什么是Spring的上下文加载器(context loader)?

见名知意,上下文加载程序负责构建应用程序上下文。我们可以通过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)绑定到调度程序特定的上下文中
  • 自动创建上下文(程序员不需要编写任何东西来使上下文工作)

Spring的上下文加载器详解

我们已经了解了上下文加载器的作用。现在,我们来更详细地介绍这其中的细节。web上下文加载器(context loader)类位于org.springframework.web.context包中。主类是ContextLoaderListener,它扩展了ContextLoader类。同时实现了ServletContextListener接口。
在上下文创建时调用的方法是public void contextInitialized(ServletContextEvent event)。它通过传递给它所接收到的servlet上下文(从事件参数获取event.getServletContext())来调用ContextLoaderinitWebApplicationContext方法。initWebApplicationContext方法进行的第一个操作是检查是否有另一个根上下文存在。如果至少存在另一个,则抛出IllegalStateException,并且初始化失败。否则,它继续初始化org.springframework.web.context.WebApplicationContext实例。如果初始化的实例实现了ConfigurableWebApplicationContext接口,则在设置当前应用程序上下文之前,加载器将进行一些设置服务(父上下文,应用程序上下文,servlet上下文等),并通过上下文的refresh()方法来准备bean,这已经在关于应用程序上下文的文章中介绍过了。
org.springframework.web.context.ContextLoaderListener:

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

org.springframework.web.context.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) {
    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中的源码,下面相关部分已贴出。
  • 调用ContextCleanupListener.cleanupAttributes(event.getServletContext()),它将查找当前servlet上下文的所有实现org.springframework.beans.factory.DisposableBean接口的对象。之后,将调用它们的destroy()方法,以销毁不再使用的bean。

org.springframework.web.context.ContextLoaderListener:

/**
 * Close the root web application context.
 */
@Override
public void contextDestroyed(ServletContextEvent event) {
    closeWebApplicationContext(event.getServletContext());
    ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

org.springframework.web.context.ContextLoader:

/**
 * 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();
        }
    }
}

org.springframework.context.support.AbstractApplicationContext:

/**
 * 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.
}

在Spring Web应用程序中实现上下文加载程序

想象一下,你希望在系统的所有用户之间共享一个信息。你可以用传统的方式做到这一点,也可以使用你定义的上下文加载器。我们通过写一些简单的代码来达到这个目的。还有一个想要实现的功能会涉及多个上下文。我们的应用程序将同时处理guestconnected两种形式(请同时看下面源码)。可以看到他们的网页的URL匹配规则不一样。使用connected的用户将能够访问与guest规则下以.chtml扩展名结尾的相同的页面,也就是所谓的交集。需要说的是,他们不会共享相同的信息(两个不一样的上下文当然不会一样了)。还不懂的话看下面源码,对于这两者,我们将分别 指定两个servlet上下文。你会看到,因为它,访问connected用户将不会与访问guest共享相同的bean。
我们将从web.xml文件开始,请对比上面说的:

<!--?xml version="1.0" encoding="UTF-8"?-->
<web-app id="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>
  <-- guest is the default servlet -->
  <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>
  <-- Customized listener which will put some personnalized data into servlet's context -->
  <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:

<bean id="secretData" class="com.migo.secret.SecretData">
  <property name="question" value="How old are you ?"/>
  <property name="answer" value="33"/>
</bean>
<context:annotation-config/>
<context:component-scan base-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
  public String toString() {
    return "SecretData {question: "+this.question+", answer: "+this.answer+"}";
  }
}

其他Java代码也很简单。在CustomizedContextLoader中,我们重写contextInitialized方法来放置共享servlet的上下文属性:名字叫webappVersion。该属性是一个随机数,用于证明根应用程序上下文的加载程序仅被调用一次:

public class CustomizedContextLoader extends 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)
    public String test(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)
    public String guestTest(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的可见性。Guestservlet没有看到secretData bean,因为它仅在connected (connected-servlet.xml)的配置中被定义。

总结

第一部分涉及了这个加载器的两个主要角色:将根Web应用程序上下文(Root WebApplicationContext)绑定到调度程序特定的上下文中并自动创建上下文。接下来,我们分析了关于上下文加载程序的代码的要点所涉及的细节,如所实现的接口和主要方法的细节实现。最后一部分是我们自定义扩展本地上下文加载器,然后对bean和servlet的属性继承方面进行一些测试。

作者:一叶知秋

来源:https://muyinchen.github.io/2017/09/12/Spring5%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-Spring%E4%B8%AD%E7%9A%84Context%20loader/


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (0)