Spring MVC 初始化源码—ContextLoaderListener监听器与根上下文容器的初始化

Posted L-Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC 初始化源码—ContextLoaderListener监听器与根上下文容器的初始化相关的知识,希望对你有一定的参考价值。

  基于最新Spring 5.x,详细介绍了Spring MVC 初始化流程的源码,主要包括ContextLoaderListener与根上下文容器的初始化流程的源码,以及web.xml文件加载流程。

  此前的一系列专栏文章中:Spring MVC 5.x 学习,我们对Spring MVC 5.x的重要特性进行了学习,基本掌握了Spring MVC的基本使用,现在我们一起来尝试学习Spring MVC的源码,尝试从源码的角度再次理解Spring MVC的整体执行流程,体会组件式架构的巧妙之处!
  Spring MVC同样依赖于Spring,关于容器初始化、bean注册、对象创建等基础功能的具体源码,我们在此前的Spring源码学习部分已经花了几十万字详细讲解过了,在此不再赘述,在学习Spring MVC的源码之前建议大概了解Spring的源码

  本次主要学习web.xml文件加载流程以及ContextLoaderListener监听器的加载,即根上下文容器的初始化流程的源码。

  下面的源码版本基于5.2.8.RELEASE

Spring MVC源码 系列文章

Spring MVC 初始化源码(1)—ContextLoaderListener与根上下文容器的初始化

1 web.xml文件加载流程

  引入Spring MVC之后就Java项目就成为了一个web项目,项目启动的流程相较于此前学习的本地Spring项目变得更加复杂,我们必须找到此时的项目初始化的入口,才能更好的进行分析。
  我们的web项目实际上是一个非常被动的存在,里面没有main方法(非Spring Boot项目),它并不会自己启动,所谓的启动项目,是指的我的启动tomcat服务器,然后由tomcat服务器来对里面的web项目启动并且进行一系列初始化操作的。而tomcat服务器是通过加载项目的web.xml配置文件来启动整个项目的,因此,Spring MVC项目的启动流程可以从web.xml配置文件的加载过程中略知一二!
  无论是原始Servlet的web项目,还是SSM的web项目,tomcat加载web.xml配置文件的过程和顺序都是一样的,常见组件的通用的加载顺序如下(部分顺序涉及到tomcat的源码,后面有机会我们在学习tomcat的源码):

  1. tomcat服务器首先会初始化该项目的Context容器StandardContext,代表该web应用,并且会扫描web.xml文件中标签的数据并存入该容器的对应属性中,包括<context-param/>标签表示的容器常量。
  2. 根据扫描结果初始化web.xml中所有的定义的Listener实例。
  3. 初始化项目中使用的(代码获取到的)ServletContext容器,实际类型是一个ApplicationContextFacade(基于外观模式)。其内部封装了一个ApplicationContext实例,ApplicationContext内部封装<context-param>常量,还封装了tomcat内部的Context容器实例StandardContext,可以获取tomcat内部注册的Servlet等信息。
  4. 创建ServletContextEvent事件,其内部包含了ApplicationContextFacade容器,随后发布该事件,即对所有的ServletContextListener实例调用contextInitialized方法,可以从ServletContextEvent中获取容器初始化参数信息。
  5. 根据扫描结果初始化web.xml中所有的定义的Filter实例,并调用对应实例的init方法初始化filter。
  6. 加载和初始化load-on-startup属性大于等于0的Servelet,按照属性值的大小从小到大依次加载和初始化,随后调用init方法初始化Servlet,参数ServletConfig实际是一个StandardWrapperFacade类的对象,该类主要包含两个属性,config对应ServletConfig, 存储的实例为StandardWrappercontext对应ServletContext,存储的实际为ApplicationContext

  简单地说,主要加载顺序就是: Listener – SrvletContext – listener#contextInitialized(ServletContextEvent)– Filter – filter#init(FilterConfig) – 加载load-on-startup属性大于等于0的Servlet – Servlet#init(ServletConfig)

  下面就是一个基于Spring MVC的web.xml一种最常见配置,可以对应着上面的流程看看这些标签的加载顺序。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <display-name>Archetype Created Web Application</display-name>

    <!--配置contextConfigLocation初始化参数,指定父容器Root WebApplicationContext的配置文件 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!--加载全部配置文件-->
        <param-value>classpath:spring-config.xml</param-value>
    </context-param>
    <!--监听contextConfigLocation参数并初始化父容器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!--设置编码-->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <!--对于request和response是否强制使用指定的编码-->
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <!--Servlet WebApplicationContext子容器的配置-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc-config.xml</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

  实际上web.xml配置文件的<web-app/>标签下可以配置很多子标签,这些标签在解析时都会被加载,但是很多标签我们都是用不到的,因此我们仅仅介绍这些常见标签的加载!

2 ContextLoaderListener根上下文容器初始化

  Spring MVC项目支持父子容器,DispatcherServlet中初始化的容器作为子容器,通常用于存放三层架构中的表现层的bean,比如Controller,以及Spring MVC相关的组件bean实例,比如ViewResolver、HandlerMapping等,“子容器”一定会存在。
  而父容器通常包含web应用中的基础结构 bean,例如需要跨多个Servlet实例共享的Dao、数据库配置bean、Service等服务bean,也就是三层架构中的业务层和持久层的bean,这些 bean可以在特定Servlet 的子 WebApplicationContext 中重写(即重新声明),ContextLoaderListener这个监听器可以配置,也可以不配置,通常情况下,该标签就被用于初始化一个父容器。
  如果配置了ContextLoaderListener监听器,那么将会有一个父容器被先初始化,我们来看看它的具体流程源码。
在这里插入图片描述
  在ServletContext容器初始化之后,将会发出容器创建事件,随即触发ContextLoaderListener#contextInitialized(ServletContextEvent event)方法调用,该方法就是我们的学习Sring MVC源码的入口:

/**
 1. ContextLoaderListener的方法,源码的入口
 2. <p>
 3. 初始化一个 root WebApplicationContext
 */
@Override
public void contextInitialized(ServletContextEvent event) {
    //调用ContextLoader的initWebApplicationContext方法
    initWebApplicationContext(event.getServletContext());
}

  其内部调用的就是ContextLoader的initWebApplicationContext方法。该方法执行完毕,则项目的Root WebApplicationContext初始化完毕。

2.1 initWebApplicationContext初始化根上下文容器

  该方法还是很简单的,相比于单体项目的IOC容器的初始化,多了解析一些全局属性以及调用ApplicationContextInitializer扩展点的逻辑,大概逻辑如下:

  1. 校验如果上下文中的"org.springframework.web.context.WebApplicationContext.ROOT"属性值不为null的话,那么直接抛出异常。如果不为null,说明此前已经创建过root application context容器了,不能再次创建
  2. 如果此ContextLoader的context属性为null,那么调用createWebApplicationContext方法初始化一个WebApplicationContext,默认为XmlWebApplicationContext
  3. 调用configureAndRefreshWebApplicationContext方法配置并刷新新建的root WebApplicationContext,该方法中会解析容器配置位置属性、初始化并调用ApplicationContextInitializer的扩展点(用于自定义root context)、执行refresh刷新容器的方法。
  4. 将当前新建的Root WebApplicationContext存入servletContext的属性中,属性名为"org.springframework.web.context.WebApplicationContext.ROOT",这也是开头校验的属性,该属性不为null就说明当前项目已经初始化好了Root WebApplicationContext
//ContextLoader的相关属性

/**
 * 此加载程序管理的Root WebApplicationContext实例
 */
@Nullable
private WebApplicationContext context;

/**
 * 如果当前初始化线程的ClassLoader本身就是ContextLoader,则将新建的容器上下文设置为当前的WebApplicationContext
 */
@Nullable
private static volatile WebApplicationContext currentContext;

/**
 * ContextLoader的方法
 * <p>
 * 使用在构建时提供的ApplicationContext或根据contextClass和contextConfigLocation参数创建一个新的ApplicationContext。
 * 为给定的ServletContext初始化Spring WebApplicationContext
 *
 * @param servletContext 当前ServletContext
 * @return 新的 WebApplicationContext
 */
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    /*
     * 如果上下文中的"org.springframework.web.context.WebApplicationContext.ROOT"属性值不为null的话,那么直接抛出异常
     * 如果不为null,说明此前已经创建过root application context容器了,不能再次创建
     */
    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!");
    }

    servletContext.log("Initializing Spring root WebApplicationContext");
    Log logger = LogFactory.getLog(ContextLoader.class);
    if (logger.isInfoEnabled()) {
        logger.info("Root WebApplicationContext: initialization started");
    }
    //当前时间戳,毫秒
    long startTime = System.currentTimeMillis();

    try {
        // 将上下文存储在本地实例变量中,以确保它在ServletContext关闭时可用。

        /*
         * 1 如果context属性为null,那么初始化一个WebApplicationContext,默认为XmlWebApplicationContext
         */
        if (this.context == null) {
            this.context = createWebApplicationContext(servletContext);
        }
        //如果属于ConfigurableWebApplicationContext类型,默认属于
        if (this.context instanceof ConfigurableWebApplicationContext) {
            //强制转换为cwac
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            //确定此应用程序上下文是否处于活动状态,即,是否至少刷新一次并且尚未关闭。
            //如果上下文尚未刷新->提供诸如设置父上下文,设置应用程序上下文ID等服务。
            if (!cwac.isActive()) {
                // 如果父上下文为null
                if (cwac.getParent() == null) {
                    // 确定根Web应用程序上下文的父级,。
                    //一般来说没有父上下文,因为ContextLoader的loadParentContext方法默认直接就是返回null的
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                /*
                 * 2 配置并刷新新建的WebApplicationContext,这是核心方法
                 */
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }
        /*
         * 3 将当前新建的Root WebApplicationContext 存入servletContext的属性中
         * 属性名为"org.springframework.web.context.WebApplicationContext.ROOT"
         *
         * 这也是开头校验的属性,该属性不为null就说明当前项目已经初始化好了Root WebApplicationContext
         */
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

        //获取当前初始化线程的ClassLoader,这个classLoader一般都是WebappClassLoader
        //WebappClassLoader是tomcat提供的,每个web应用程序都有自己专用的WebappClassLoader,用于隔离web应用之间的class的影响
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        //如果当前初始化线程的ClassLoader本身就是ContextLoader的ClassLoader
        //ContextLoader的ClassLoader同样也是WebappClassLoader,这也是tomcat设置的
        if (ccl == ContextLoader.class.getClassLoader()) {
            //则将新建的容器上下文设置为当前的WebApplicationContext
            currentContext = this.context;
            //如果不是并且不为null,那么将classLoader和context设置给一个map缓存
        } else if (ccl != null) {
            currentContextPerThread.put(ccl, this.context);
        }

        if (logger.isInfoEnabled()) {
            //输出日志
            long elapsedTime = System.currentTimeMillis() - startTime;
            logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
        }
        //最后返回创建、初始化完毕的root context
        return this.context;
    } catch (RuntimeException | Error ex) {
        logger.error("Context initialization failed", ex);
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
        throw ex;
    }
}

2.1.1 createWebApplicationContext创建新WebApplicationContext

  如果当前ContextLoaderListener实例的context属性为null,那么调用createWebApplicationContext方法初始化一个WebApplicationContext,类型可以是默认上下文类XmlWebApplicationContext,也可以是自定义上下文类(如果已指定)。

  初始化容器时,调用的是无参构造器,此时可以说是仅仅创建了一个WebApplicationContext对象,并没有进行一系列的初始化操作。

/**
 * ContextLoader的方法
 * <p>
 * 实例化此加载器的root WebApplicationContext,类型可以是默认上下文类,也可以是自定义上下文类(如果已指定)。
 * <p>
 * 指定的上下文类期望是实现了ConfigurableWebApplicationContext接口
 * 另外,在刷新上下文之前会调用customContext方法,从而允许子类对上下文执行自定义修改。
 *
 * @param sc 当前ServletContext
 * @return root WebApplicationContext
 */
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    //返回要使用的WebApplicationContext实现类的Class
    Class<?> contextClass = determineContextClass(sc);
    //如果对应的Class不是ConfigurableWebApplicationContext类型,那么抛出异常
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    //反射调用无参构造器初始化WebApplicationContext的实例并返回
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

2.1.1.1 determineContextClass获取上下文的Class

  该方法获取要使用的上下文的Class,首先采用自定义的WebApplicationContext类型,这是通过名为"contextClass"的<context-param>全局初始化参数指定的,如果没有该参数,那么将使用默认的WebApplicationContext,即org.springframework.web.context.support.XmlWebApplicationContext,这是在ContextLoader同路径下的ContextLoader.properties配置文件中定义的。
  也就是说,我们可以通过在web.xml文件中定义一个param-namecontextClass<context-param/>标签来指定自定义的容器类型,param-value就是自定义的容器的全路径名字符串

//----------ContextLoader的属性和静态块-----------

/**
 * 要使用的root WebApplicationContext实现类的配置参数:"contextClass"
 * 通过该全局参数可以指定一个自定义WebApplicationContext实现类的全路径名
 */
public static final String CONTEXT_CLASS_PARAM = "contextClass";

/**
 * 定义ContextLoader的默认策略名称的类路径资源的名称(相对于ContextLoader类)。
 */
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";

/**
 * 默认WebApplicationContext策略配置文件的属性集合
 */
private static final Properties defaultStrategies;

static {
    // 从属性文件加载默认策略实现。
    // 当前这严格是内部的文件,并不意味着应由应用程序开发人员自定义。
    try {
        //加载ContextLoader类路径下的ContextLoader.properties配置文件的键值对到defaultStrategies集合中
        //该配置文件中定义了默ContextLoader的默认WebApplicationContext实现类
        //默认实现类就是: org.springframework.web.context.support.XmlWebApplicationContext
        ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    } catch (IOException ex) {
        throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
    }
}

/**
 1. ContextLoader的方法
 2. <p>
 3. 返回要使用的WebApplicationContext实现类
 4. 如果未指定,则为默认XmlWebApplicationContext,或者是自定义的上下文类。
 5.  6. @param servletContext 当前ServletContext
 7. @return 使用的WebApplicationContext实现类
 */
protected Class<?> determineContextClass(ServletContext servletContext) {
    //获取名为"contextClass"的全局参数,该参数用于指定自定义的WebApplicationContext
    //一般都是没有指定的,即获取结果为null
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
    //如果不为null,说明指定了该参数
    if (contextClassName != null) {
        try {
            //获取自定义的WebApplicationContext的Class
            return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
        } catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load custom context class [" + contextClassName + "]", ex);
        }
    }
    //如果为null,说明没有指定该参数,将使用默认WebApplicationContext实现类
    else {
        //从defaultStrategies集合中获取名为org.springframework.web.context.WebApplicationContext的属性值
        //默认实现类就是: org.springframework.web.context.support.XmlWebApplicationContext
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        try {
            //获取默认的XmlWebApplicationContext的Class
            return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
        } catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load default context class [" + contextClassName + "]", ex);
        }
    }
}

  ContextLoader.properties如下:
在这里插入图片描述

2.1.1.2 XmlWebApplicationContext

  XmlWebApplicationContextorg.springframework.web.context.WebApplicationContext的一种实现,该实现从XML文档获取配置。它的uml类图如下:
在这里插入图片描述
  在最开始学习源码的时候我们就已经介绍了Spring的ApplicationContext体系,在此对于学习过的类不再赘述。
  WebApplicationContext继承了ApplicationContext接口,来自于spring-web依赖,实现该接口的容器专门用于基于Servlet的web项目。该接口相比于ApplicationContext,多了一个getServletContext方法,即获取Servlet上下文。因此,除了标准的ApplicationContext生命周期功能外,WebApplicationContext的实现还需要检测ServletContextAware Bean并相应地调用setServletContext方法。
在这里插入图片描述
  可配置的WebApplicationContext需要实现ConfigurableWebApplicationContext的接口,该接口提供了设置ServletContext、ServletConfig、Namespace、ConfigLocation的方法,可以对上下文进行自定义配置。
在这里插入图片描述
  AbstractRefreshableWebApplicationContext是ConfigurableWebApplicationContext的骨干实现,提供了各种属性用来保存配置的数据。
在这里插入图片描述
  同时它还继承了AbstractRefreshableConfigApplicationContext,因此是一个可刷新的ApplicationContext,继承了此前讲过的容器初始化的所有的功能。
  XmlWebApplicationContextorg.springframework.web.context.WebApplicationContext的一种可用实现,该实现从XML文档获取配置。默认情况下,将从“/WEB-INF/applicationContext.xml”获取根上下文的配置路径,从“/WEB-INF/test-servlet.xml”获取具有“test-servlet” 名称空间的子上下文的配置路径(例如servlet-name为“test”的DispatcherServlet实例)。
  可以通过org.springframework.web.context.ContextLoadercontextConfigLocation上下文参数(即全局<context-param/>配置的contextConfigLocation参数)和org.springframework.web.servlet.FrameworkServletcontextConfigLocation参数(即servlet内部的<init-param/>配置的contextConfigLocation参数)覆盖配置位置的默认值。配置的位置值可以表示“/WEB-INF/context.xml”之类的某个具体文件,也可以使用“/WEB-INF/*-context.xml”之类的Ant样式的模式匹配多个文件。
  如果有多个配置位置,则较新的Bean定义将覆盖较早加载的文件中的定义,可以利用它来通过一个额外的XML文件有意覆盖某些bean定义。

2.1.2 configureAndRefreshWebApplicationContext配置并刷新容器

  在创建了空容器之后(默认是XmlWebApplicationContext),将会调用configureAndRefreshWebApplicationContext方法配置并刷新该容器。
  该方法执行完毕,则新建的WebApplicationContext容器配置并初始化完毕。
  主要有如下步骤:

  1. 设置该应用程序上下文的id(一般用不到),默认id就是“org.springframework.web.context.WebApplicationContext:”+项目路径,可以通过在web.xml中配置名为contextId<context-param/>全局参数来自定义Root容器id。
  2. ServletContext设置给该容器的servletContext属性。
  3. 设置容器配置信息。首先获取名为contextConfigLocation的全局属性,如果配置了该属性,那么该属性的值将作为配置文件的路径,随后就调用setConfigLocation方法解析传入的配置值,用以设置容器配置信息(配置值支持按照",; \\t\\n"来拆分)。
  4. 获取容器的Environment环境变量对象,随后调用initPropertySources方法手动初始化Servlet属性源,该方法在refresh()刷新容器的方法之前执行,以确保servlet属性源已准备就绪,可以被refresh()方法正常使用。
  5. 调用customizeContext方法用于对容器执行自定义操作。默认实现是通过web.xml中配置的全局参数contextInitializerClasses和globalInitializerClasses来确定指定了哪些ApplicationContextInitializer类,并执行初始化,随后使用AnnotationAwareOrderComparator排序(支持PriorityOrdered接口、Ordered接口、@Ordered注解、@Priority注解的排序),最后按照排序优先级从高到低一次调用每一个实例的initialize方法来初始化给定的servletContext
  6. 调用容器的refresh方法执行刷新操作,这是核心方法,我们在此前IoC容器初始化源码部分已经着重讲解了。该方法将会初始化容器,包括解析配置文件,创建Spring bean实例,执行各种回调方法等等……操作(源码非常多)
//ContextLoader的常量属性

/**
 * Root WebApplicationContext ID的配置参数,用作基础BeanFactory的序列化ID:"contextId"。
 */
public static final String CONTEXT_ID_PARAM = "contextId";

/**
 * 指定Root WebApplicationContext的配置文件位置的servletContext参数的名称
 * 即"contextConfigLocation",如果不存在该参数,则查找默认值。
 */
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";

以上是关于Spring MVC 初始化源码—ContextLoaderListener监听器与根上下文容器的初始化的主要内容,如果未能解决你的问题,请参考以下文章

Spring MVC 初始化源码—@RequestMapping注解的源码解析

Spring MVC源码——Root WebApplicationContext

Spring6源码・MVC请求处理流程源码解析

Spring MVC 初始化源码—DispatcherServlet与子容器的初始化以及MVC组件的初始化一万字

一文彻底解密Spring 源码之Spring MVC

一文彻底解密Spring 源码之Spring MVC