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

漫谈Spring的启动与初始化

Spring winrains 来源:陈恭帅 1年前 (2019-10-22) 56次浏览

最近帮实验室同学调了个BUG,项目中也要用到Spring,想想虽然之前也有看过一些关于Spring容器的知识,但是有些东西却已经逐渐生疏了或者是模糊了,比如最常见的问题,Spring的启动流程是什么样的?这个问题,我想即使是经常做web开发的也不见得能完完整整的回答出来,于是结合网上资料再配合Spring源码,决定再梳理一下Spring容器是如何启动并初始化的,如果有不正确的地方,希望路过的可以指出。

从Tomcat启动开始

查阅了大部分网上的文章,基本90%的都是直接从ContextLoaderListener这个监听器开始谈起,说ContextLoaderListener监听到了ServletContext的启动然后balabala开始启动容器了,但是我想知道监听器是什么时候来的啊,一切总是有个开始的,不能说有个监听器就有个监听器了吧。

初始Tomcat的基本结构

因为web最终是运行在类似于Tomcat这种web容器里面的,它对于web的生命周期应该有着直接的关系,所以我决定从Tomcat开始看起,下面是Tomcat的容器模型:

从上图可以看出 Tomcat 的容器分为四个等级,真正管理 Servlet 的容器是 Context 容器,一个 Context 对应一个 Web 工程。尤其是后面一句话,一个 Context 对应一个 Web 工程,所以我们在Tomcat根目录的webapps文件夹路径下面经常会看到除了我们自己部署的web,还有若干其他Tomcat自带的web,不同的web工程都会对应在Tomcat里面的context容器。

Tomcat的启动

先看这张图,Tomcat主要类的启动时序图:

可以看到Tomcat的启动是从顶层开始一直到到EngineHost再到StandardContext(这里的context我可以理解为就是对应每个web工程的容器了),之前看过一些Tomcat的源码,Tomcat是采用了一种观察者模式的设计方式,所有的容器都会继承 Lifecycle 接口,它管理容器的整个生命周期,所有容器的的修改和状态的改变都会由它去通知已经注册的观察者(Listener)。不清楚的可以参考Tomcat生命周期管理
说了这么多还没到我们配置在web.xml里面的监听器,快了。当 Context 容器初始化状态设为 init 时,添加在 Context 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口,ContextConfig 类会负责整个 Web 应用的配置文件的解析工作。

web应用的初始化

Web 应用的初始化工作是在 ContextConfigconfigureStart 方法中实现的,应用的初始化主要是要解析 web.xml 文件,这个文件描述了一个 Web 应用的关键信息,也是一个 Web 应用的入口。web.xml 文件中的各个配置项将会被解析成相应的属性保存在 WebXml 对象中,接下去将会将 WebXml 对象中的属性设置到 Context 容器中,这里包括创建 Servlet 对象、filter、listener 等等。
所有 web.xml 属性都被解析到 Context 中,所以说 Context 容器才是真正运行 Servlet 的 Servlet 容器。一个 Web 应用对应一个 Context 容器,容器的配置属性由应用的 web.xml 指定,这样我们就能理解 web.xml 到底起到什么作用了。

再谈Spring的启动

启动Spring之前

从上面的流程中,我们知道web.xml配置的最常见的ContextLoaderListener监听器是从Tomcat启动时候创建的。根据以上Tomcat的启动背景,我们先尝试简单梳理从Tomcat读取web.xml配置文件到启动Spring的一系列流程。

  1. 在启动Web项目时,容器比如Tomcat会读web.xml配置文件中所有的属性,包括contextConfigLocationContextLoaderListener这些等等。
  2. 接着Tomcat会创建一个ServletContext(这里的ServletContext可以理解为上述每个web应用对应的Context容器),应用范围内即整个web项目都能使用这个上下文。
  3. Tomcat将刚刚读取到contextConfigLocationContextLoaderListener这些web.xml中的参数键值对交给ServletContext
  4. 创建web.xml 中配置的监听器类Listener。在监听器类中必须要实现ServletContextListener接口,接口源代码如下:
    public interface ServletContextListener extends EventListener {
        //初始化Spring容器
        void contextInitialized(ServletContextEvent var1);
        //用于关闭应用前释放资源,比如说数据库连接的关闭。
        void contextDestroyed(ServletContextEvent var1);
    }
  5. 注意,这个时候才刚刚开始,你的web项目还没有完全启动完成,包括Spring还没启动。

由ContextLoaderListener监听器启动Spring

因为ContextLoaderListener实现了用来监听ServletContext事件的ServletContextListener 这个接口,如果 ServletContext 状态发生变化,将会触发产生对应的ServletContextEvent,然后调用监听器的不同的方法。ContextLoaderListener结构如下:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    ...
    //实现了ServletContextListener的初始化接口
    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }
    //同上
    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

最终上面的initWebApplicationContextContextLoader来完成,这个时候就真正开始了Spring容器的初始化了,真是个激动人心的时刻,百转千回。
下文将仔细介绍initWebApplicationContext是如何进行初始化的。

initWebApplicationContext方法

ContextLoaderListener监听到ServletContext初始化事件的时候就会调用ContextLoaderinitWebApplicationContext方法,这个方法完成了很多的工作,其中便有下面这段关键代码,源码如下:

private WebApplicationContext context;
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    ...
    if(this.context == null) {
        //如果context为null就开始创建WebApplicationContext
        this.context = this.createWebApplicationContext(servletContext);
    }
    ...
}

createWebApplicationContext方法

下面进入该方法,源码如下:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    Class contextClass = this.determineContextClass(sc);
    if(!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    } else {
        //根据类返回实例
        return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
    }
}

其中determineContextClass(sc)方法是用来寻找实现WebApplicationContext接口的类,实现方法如下:

protected Class<?> determineContextClass(ServletContext servletContext) {
    //关键代码一:
    String contextClassName = servletContext.getInitParameter("contextClass");
    if(contextClassName != null) {
        try {
            return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
        } catch (ClassNotFoundException var4) {
            throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]", var4);
        }
    } else {
        //关键代码二:
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        try {
            return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
        } catch (ClassNotFoundException var5) {
            throw new ApplicationContextException("Failed to load default context class [" + contextClassName + "]", var5);
        }
    }
}

上面这两行代码就是决定返回的Class:如果开发人员在web.xml中配置了一个参数名为contextClass,值为WebApplicationContext接口实现类,那getInitParameter("contextClass")就会返回这个配置的实现类Class;如果没有配置,也就是contextClassName==null,那么通过defaultStrategies.getProperty(...)则会返回Spring默认的实现类XmlWebApplicationContext。可能有同学会好奇为什么是这个,这个类是从哪儿来的。
我们回头在ContextLoader这个类中最下面可以看到有这么几行静态代码段,在类一加载的时候执行,如下:

public class ContextLoader {
    private static final Properties defaultStrategies;
    static {
        try {
            ClassPathResource ex = new ClassPathResource("ContextLoader.properties", ContextLoader.class);
            defaultStrategies = PropertiesLoaderUtils.loadProperties(ex);
        } catch (IOException var1) {
            throw new IllegalStateException("Could not load \'ContextLoader.properties\': " + var1.getMessage());
        }
        currentContextPerThread = new ConcurrentHashMap(1);
    }
}

ContextLoader.class的同一个包下面,可以找到这个配置文件,其中只有一行配置如下:

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

XmlWebApplicationContext这个类就是WebApplicationContext这个接口最终的实现类,也是Spring启动时默认使用的类。其实还有一些实现类,让我们自己去加载applicationContext.xml,比如ClassPathXmlApplicationContext
这样在上述的createWebApplicationContext方法中,我们拿到的就是XmlWebApplicationContext.class,然后通过BeanUtils.instantiateClass(contextClass)方法根据类名创建对应实例,并且进行强制转换得到ConfigurableWebApplicationContext接口的实例,因为XmlWebApplicationContext是后者的实现类,所以这样转换是没问题的(当然没问题哈哈)。
那么createWebApplicationContext方法分析到此为止。
我们继续看initWebApplicationContext方法中下面这段代码的最后一行:

if(this.context == null) {
    //如果context为null就开始创建WebApplicationContext
    this.context = this.createWebApplicationContext(servletContext);
}
if(this.context instanceof ConfigurableWebApplicationContext) {
    ConfigurableWebApplicationContext err = (ConfigurableWebApplicationContext)this.context;
    if(!err.isActive()) {
        ...
        //关键代码:配置和刷新WebApplicationContext,这里其实就是XmlWebApplicationContext了
        this.configureAndRefreshWebApplicationContext(err, servletContext);
    }
}

configureAndRefreshWebApplicationContext方法

在上面代码中我们通过XmlWebApplicationContext类创建了WebApplicationContext的实例,本节方法则是为该实例设置一些配置信息和创建各种bean。我们重点关注下面几行代码:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    ...
    wac.setServletContext(sc);
    //关键代码一:在ServletContext中获取contextConfigLocation值,查找配置文件地址
    configLocationParam = sc.getInitParameter("contextConfigLocation");
    if(configLocationParam != null) {
        wac.setConfigLocation(configLocationParam);
    }
    ...
    //关键代码二:刷新XmlWebApplicationContext
    wac.refresh();
}

在Spring的项目中,经常要在web.xml中配置contextConfigLocation的参数,我们对于下面的几行xml代码应该也很熟悉,它设置了Spring容器配置文件的路径:

<!-- Context ConfigLocation -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:/spring-context*.xml</param-value>
</context-param>

既然是在web.xml中配置的这些参数,为什么是在ServletContext中去取呢?在上一篇文章中分析过,Tomcat在加载web.xml文件时,会最终将该配置文件的配置属性参数以键值对的形式存放在每个web应用对应的ServletContext中,这样我们在web应用中的任何地方都可以拿到该参数值(后面还会提到ServletContext其他的使用场景)。
当然,如果我们没有在web.xml中配置该参数的话,XmlWebApplicationContext类也是有默认值的,如下:

public class XmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
    public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
    public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";
    public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";
}

到这里,配置文件信息拿到了,再就是wac.refresh()方法了,这个方法具体实现在AbstractApplicationContext类中,进入该方法:

public void refresh() throws BeansException, IllegalStateException {
    Object var1 = this.startupShutdownMonitor;
    synchronized(this.startupShutdownMonitor) {
       //容器预先准备,记录容器启动时间和标记
      prepareRefresh();
      //创建bean工厂,里面实现了BeanDefinition的装载
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
      //配置bean工厂的上下文信息,如类装载器等
      prepareBeanFactory(beanFactory);
      try {
        //在BeanDefinition被装载后,提供一个修改BeanFactory的入口
        postProcessBeanFactory(beanFactory);
        //在bean初始化之前,提供对BeanDefinition修改入口,PropertyPlaceholderConfigurer在这里被调用
        invokeBeanFactoryPostProcessors(beanFactory);
        //注册各种BeanPostProcessors,用于在bean被初始化时进行拦截,进行额外初始化操作
        registerBeanPostProcessors(beanFactory);
        //初始化MessageSource
        initMessageSource();
        //初始化上下文事件广播
        initApplicationEventMulticaster();
        //模板方法
        onRefresh();
        //注册监听器
        registerListeners();
        //初始化所有未初始化的非懒加载的单例Bean
        finishBeanFactoryInitialization(beanFactory);
        //发布事件通知
        finishRefresh();
        } catch (BeansException var5) {
            if(this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var5);
            }
            this.destroyBeans();
            this.cancelRefresh(var5);
            throw var5;
        }
    }
}

这个方法里面就是IOC容器初始化的大致步骤了。

after configureAndRefresh

在IOC容器初始化之后,也就是configureAndRefreshWebApplicationContext方法执行结束后有一行代码如下:

//关键代码:
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

可以看到,这里初始化后的context被存放到了servletContext中,具体的就是存到了一个Map变量中,key值就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE这个常量。这个key常量在WebApplicationContext接口中设置的,如下:

public interface WebApplicationContext extends ApplicationContext {
    //org.springframework.web.context.WebApplicationContext.ROOT
    String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
}

另外,我们也可以使用Spring的WebApplicationContextUtils工具类获取这个WebApplicationContext(不过这里request获取ServletContext是有限制的,要求servlet-api.jar 包的版本是在3.0以上)方式如下:

WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());

到这里Spring的启动与初始化应该就结束了,这里面要理清ServletContext和Spring容器的关系,整个Spring容器被放置在ServletContext这样一个类似于Map的结构中。ServletContext 从字面上理解也是Servlet的容器,被 Servlet 程序间接用来与外层 Web 容器通信,例如写日志,转发请求等。每一个 Web 应用程序含有一个Context ,被Web 应用内的各个程序共享。因为Context 可以用来保存资源并且共享,所以ServletContext 的常用场景是Web级应用缓存—- 把不经常更改的内容读入内存,所以服务器响应请求的时候就不需要进行慢速的磁盘I/O 了。

参考

Servlet 工作原理解析
spring项目中监听器作用-ContextLoaderListener

作者:陈恭帅

来源:
https://zouzls.github.io/2017/03/29/SpringStart/
http://zouzls.github.io/2017/08/06/SpringInit/


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