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

Spring 5.x 源码 专栏收录该内容
32 篇文章 21 订阅

  基于最新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与父上下文容器的初始化

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

Spring MVC 初始化源码(3)—<mvc:annotation-driven >配置标签的源码解析

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

Spring MVC 请求执行流程的源码深度解析【两万字】

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";


/**
 * ContextLoader的方法
 * <p>
 * 配置并刷新给定的WebApplicationContext
 *
 * @param wac WebApplicationContext
 * @param sc  ServletContext
 */
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    /*
     * 1 如果wac的全路径identity字符串形式等于wac的id,那么设置应用程序上下文的id
     */
    //创建WebApplicationContext时,id默认就是ObjectUtils.identityToString的值,因此一般都相等
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        //获取名为contextId的全局属性值
        String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
        if (idParam != null) {
            //如果设置了该属性,那么将该属性的值设置为id
            wac.setId(idParam);
        } else {
            //如果没有设置该属性,那么设置默认id: 全路径字符串+":"+项目路径,比如项目路径是"/mvc"
            //那么id就是,org.springframework.web.context.WebApplicationContext:/mvc
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(sc.getContextPath()));
        }
    }
    /*
     * 2 将ServletContext设置给servletContext属性
     */
    wac.setServletContext(sc);
    /*
     * 3 设置容器配置信息
     */
    //获取名为contextConfigLocation的全局属性值
    String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
    if (configLocationParam != null) {
        /*
         * 如果具有该属性,那么该属性的值将作为配置文件的路径
         * 调用setConfigLocation方法设置容器配置信息,该方法的源码我们在IOC源码的开头就讲过了
         *
         * 该方法中将会通过getEnvironment方法初始化当前上下文的可配置的环境变量对象environment。
         * 实际类型为StandardServletEnvironment
         */
        wac.setConfigLocation(configLocationParam);
    }

    /*
     * 4 获取容器的Environment环境变量对象,随后调用initPropertySources方法手动初始化属性源
     * 该方法在refresh()刷新容器的方法之前执行,以确保servlet属性源已准备就绪,可以被正常使用
     */
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        //由于是Root Context,因此只初始化ServletContext属性源
        ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
    }
    /*
     * 5 自定义容器
     *
     * 对当前的WebApplicationContext实例应用给定的ApplicationContextInitializer,以实现自定义上下文的逻辑
     */
    customizeContext(sc, wac);
    /*
     * 6 刷新(初始化)容器
     * 这是核心方法,我们在此前IoC容器初始化源码部分已经着重讲解了
     */
    wac.refresh();
}

2.1.2.1 配置文件路径

  configureAndRefreshWebApplicationContext方法中会解析自定义的配置文件路径(如果配置了contextConfigLocation属性),而在后续loadBeanDefinitions加载bean定义的方法中将会调用getConfigLocations方法获取配置文件路径。

  如果配置了contextConfigLocation属性,那么就是根据该属性的值指定的配置文件路径来初始化。
  如果没有配置该属性,那么就会调用getDefaultConfigLocations方法获取默认路径:

  1. 对于Root WebApplicationContext,默认路径为"/WEB-INF/applicationContext.xml"。
  2. 对于DispatcherServlet绑定的子WebApplicationContext,默认路径为"/WEB-INF/"+容器nameSpace+ ".xml"。
  3. Root容器没有nameSpace(为null),MVC子容器则拥有。DispatcherServlet对应的子容器的nameSpace就等于DispatcherServlet的namespace,可以通过设置Servlet的nameSpace属性手动指定名称空间,如未指定,那么默认名称空间为servletName+"-servlet",即如果此servlet的servlet-name为"test",则该servlet使用的默认名称空间将解析为"test-servlet"。如果也未指定该Servlet的contextConfigLocation属性,那么最终的默认配置路径就是"/WEB-INF/test-servlet.xml"。

  是不是和其他文章中常说的默认路径有些不一样 ?还有些复杂,是的,真正的规则就是这样的!

//XmlWebApplicationContext中的常量属性

/**
 * root context的默认配置位置
 */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";

/**
 * 用于为namespace构建配置位置的默认前缀
 */
public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";

/**
 * 用于为namespace构建配置位置的默认后缀
 */
public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";

/**
 * XmlWebApplicationContext的方法
 * <p>
 * 如果没有指定配置路径,那么使用默认配置路径
 * <p>
 * root context的默认路径是"/WEB-INF/applicationContext.xml"
 * DispatcherServlet context的默认路径是"/WEB-INF/"+nameSpace+".xml"
 * root context没有nameSpace属性(为null),该属性只有 DispatcherServlet关联的容器会配置
 */
@Override
protected String[] getDefaultConfigLocations() {
    if (getNamespace() != null) {
        return new String[]{DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
    } else {
        return new String[]{DEFAULT_CONFIG_LOCATION};
    }
}

  DispatcherServlet对应的容器的nameSpace就等于DispatcherServlet的namespace,可以通过设置Servlet的nameSpace属性手动指定名称空间,如未指定,那么默认名称空间为servletName+"-servlet",即如果此servlet的servlet-name为"test",则该servlet使用的默认名称空间将解析为"test-servlet"。如果也未指定该Servlet的contextConfigLocation属性,那么最终的默认配置路径就是"/WEB-INF/test-servlet.xml"。

/**
 * FrameworkServlet中的常量属性
 * <p>
 * WebApplicationContext名称空间的后缀。
 * 如果此类的servlet在上下文中被命名为"test",则servlet使用的默认名称空间将解析为"test-servlet"。
 */
public static final String DEFAULT_NAMESPACE_SUFFIX = "-servlet";

/**
 * FrameworkServlet的方法
 * <p>
 * 获取此nameSpace
 * <p>
 * 返回此servlet的名称空间,如果未设置自定义名称空间,则返回默认方案:
 * 即默认nameSpace为servletName+"-servlet",也可以通过设置Servlet的nameSpace属性手动指定名称空间
 */
public String getNamespace() {
    return (this.namespace != null ? this.namespace : getServletName() + DEFAULT_NAMESPACE_SUFFIX);
}

2.1.2.2 StandardServletEnvironment环境对象

  setConfigLocation方法的源码我们在IOC源码的开头文章中就讲过了,该方法中将会通过getEnvironment方法初始化当前上下文的可配置的环境变量对象environment,实际类型为StandardServletEnvironment。另外,就算没有设置contextConfigLocation方法,也会在之后的getEnvironment方法中初始化。

  StandardServletEnvironment是基于Servlet的Web应用程序将使用的环境实现。默认情况下,所有与Web相关的(基于Servlet的)ApplicationContext类都将初始化一个实例。它的uml类图如下:
在这里插入图片描述
  StandardServletEnvironment继承了StandardEnvironment,同时实现了ConfigurableWebEnvironment接口,该接口提供了一个initPropertySources方法,用于初始化ServletContext和ServletConfig中提供的属性源。
  StandardServletEnvironment创建的时候,在调用父类AbstractEnvironment的构造器的时候,将会执行customizePropertySources方法,该方法被StandardServletEnvironment重写,用于初始化默认的一系列属性源!相比于父类StandardEnvironment,除了JVM系统属性源和系统环境属性源之外,额外提供了ServletConfig,ServletContext和基于JNDI的属性源

//StandardServletEnvironment的属性常量

/**
 * ServletContext初始化参数属性源名称
 */
public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";

/**
 * ServletConfig初始化参数属性源名称
 */
public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";

/**
 * JNDI属性源名称
 */
public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";


/**
 * 使用父类提供的属性源以及适用于基于标准servlet的环境的属性源来定制属性源集合:
 * "servletConfigInitParams"
 * "servletContextInitParams"
 * "jndiProperties"
 * <p>
 * "servletConfigInitParams"中存在的属性将优先于"servletContextInitParams"中的属性,
 * 而在上述任何一个中找到的属性都将优先于在"jndiProperties"中找到的属性。
 * 而以上任何一项中的属性都将优先于StandardEnvironment超类提供的JVM系统属性源和系统环境属性源。
 * <p>
 * 与Servlet相关的属性源在此阶段将被添加一个空的属性源,并且在实际的ServletContext对象可用时将被完全初始化。
 */
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
    //空的servletConfigInitParams属性源添加到集合尾部
    propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
    //空的servletContextInitParams属性源添加到集合尾部
    propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
    //空的jndiProperties属性源添加到集合尾部
    if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
        propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
    }
    //调用父类StandardEnvironment的方法继续添加systemProperties和systemEnvironment属性源到尾部
    super.customizePropertySources(propertySources);
}


//StandardEnvironment的属性常量


/**
 * 系统环境属性源名称
 */
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

/**
 * JVM系统属性属性源名称
 */
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";


/**
 * 使用适用于任何标准Java环境的属性定制属性源集:
 * "systemProperties"
 * "systemEnvironment"
 * <p>
 * "systemProperties"中存在的属性将优先于"systemEnvironment"中的同名属性。
 */
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
    //首先添加systemProperties属性
    propertySources.addLast(
            new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
    //其次添加systemEnvironment属性
    propertySources.addLast(
            new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}

  对此,我们知道,基于Servlet的web项目中的默认属性源的查找优先级为:ServletConfig初始化参数属性源 > ServletContext初始化参数属性源 > JNDI属性源 > JVM系统属性属性源 > 系统环境属性源。如果存在同名属性,那么优先级最高的属性源中的属性值将被使用。

2.1.2.3 initPropertySources初始化Servlet属性源

  该方法用于初始化Servlet相关的属性源。实际上就是对这钱加入的空数据源替换为真正包含数据的属性源,顺序没有变。

/**
 * StandardServletEnvironment的方法
 * <p>
 * 初始化Servlet属性源
 *
 * @param servletContext servletContext
 * @param servletConfig  servletConfig
 */
@Override
public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
    WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
}


/**
 * WebApplicationContextUtils
 * <p>
 * 将基于Servlet的空属性源替换为使用给定ServletContext和ServletConfig对象填充的实际属性源实例。
 * <p>
 * 此方法可以调用任意次,它是幂等的,因为将用其相应的实际属性源只会执行一次且仅一次的空属性源替换。
 *
 * @param sources        需要初始化的属性源集合
 * @param servletContext 当前的ServletContext(如果为null或servlet上下文属性源已经初始化,则将其忽略)
 * @param servletConfig  当前的ServletConfig(如果为null或servlet config属性源已经初始化,则将其忽略)
 */
public static void initServletPropertySources(MutablePropertySources sources,
                                              @Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
    Assert.notNull(sources, "'propertySources' must not be null");
    //初始化servletContextInitParams属性源
    String name = StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME;
    if (servletContext != null && sources.get(name) instanceof StubPropertySource) {
        //存入ServletContextPropertySource
        sources.replace(name, new ServletContextPropertySource(name, servletContext));
    }
    //初始化servletConfigInitParams属性源
    name = StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME;
    if (servletConfig != null && sources.get(name) instanceof StubPropertySource) {
        //存入ServletConfigPropertySource
        sources.replace(name, new ServletConfigPropertySource(name, servletConfig));
    }
}

2.1.2.4 customizeContext应用ApplicationContextInitializer扩展

  refresh方法执行之前,会调用customizeContext方法用于对IOC容器执行自定义操作。
  默认实现是通过上下文初始化参数contextInitializerClasses和globalInitializerClasses来确定指定了哪些ApplicationContextInitializer类,并执行初始化,随后使用AnnotationAwareOrderComparator排序(支持PriorityOrdered接口、Ordered接口、@Ordered注解、@Priority注解的排序),最后按照排序优先级从高到低一次调用每一个实例的initialize方法来初始化给定的servletContext

/**
 * ContextLoader的属性
 * 应用于上下文的实际ApplicationContextInitializer实例
 */
private final List<ApplicationContextInitializer<ConfigurableApplicationContext>> contextInitializers =
        new ArrayList<>();

/**
 * ContextLoader的方法
 * <p>
 * 在将设置配置文件位置之后,刷新上下文之前,可以自定义由此ContextLoader创建的ConfigurableWebApplicationContext。
 * <p>
 * 默认实现通过上下文初始化参数contextInitializerClasses和globalInitializerClasses来确定指定了哪些ApplicationContextInitializer
 * 类,并执行初始化,随后排序并调用每一个实例的initialize方法来初始化给定的servlet context
 *
 * @param sc  当前 servlet context
 * @param wac 新创建的应用程序上下文
 */
protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
    /*
     * 1 获取ServletContext中通过指定属性定义的ApplicationContextInitializer的全路径类名,并且转换为Class集合
     */
    List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
            determineContextInitializerClasses(sc);

    /*
     * 2 初始化全部的ApplicationContextInitializer并存入contextInitializers缓存集合中
     */
    for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
        Class<?> initializerContextClass =
                GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
        if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
            throw new ApplicationContextException(String.format(
                    "Could not apply context initializer [%s] since its generic parameter [%s] " +
                            "is not assignable from the type of application context used by this " +
                            "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
                    wac.getClass().getName()));
        }
        this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
    }
    /*
     * 3 对该集合进行Order排序,可以支持PriorityOrdered接口、Ordered接口、@Ordered注解、@Priority注解的排序
     * 比较优先级为PriorityOrdered>Ordered>@Ordered>@Priority,排序规则是order值越小排序越靠前,优先级越高
     * 没有order值则默认排在尾部,优先级最低。
     */
    AnnotationAwareOrderComparator.sort(this.contextInitializers);
    /*
     * 4 按照优先级从高到低依次调用ApplicationContextInitializer的initialize方法,传递的参数就是当前的应用程序上下文容器
     */
    for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
        initializer.initialize(wac);
    }
}
2.1.2.4.1 determineContextInitializerClasses确定初始化器的Class

  该方法通过获取servletContext中的contextInitializerClasses和globalInitializerClasses属性来确定指定的ApplicationContextInitializer的Class

  也就是说,我们可以通过在web.xml中定义名为contextInitializerClasses和globalInitializerClasses<context-param/>全局参数来指定自定义的ApplicationContextInitializer的全路径名,从而实现对Root Context的自定义的逻辑,并且支持使用",; \t\n"来分隔。
  这些属性用于自定义扩展的逻辑,一般来说都是很少有人配置的。

//ContextLoader的常量属性

/**
 * ApplicationContextInitializer类的配置参数,用于初始化root Web应用程序上下文:contextInitializerClasses
 */
public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses";

/**
 * 全局的ApplicationContextInitializer类的配置参数,用于初始化当前应用程序中的所有Web应用程序上下文:globalInitializerClasses
 */
public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses";


/**
 * 在单个init-param字符串值中,任意数量的这些字符都被视为多个值之间的定界符。
 * 即在init-param的值中支持使用下面的字符来区别多个字符串
 */
private static final String INIT_PARAM_DELIMITERS = ",; \t\n";

/**
 * ContextLoader的方法
 * <p>
 * 返回ApplicationContextInitializer的实现类的Class集合
 * <p>
 * 我们可以通过定义名为contextInitializerClasses和globalInitializerClasses的全局属性来
 * 解析自定义的ApplicationContextInitializer的实现类,值要求是类的全路径名
 *
 * @param servletContext 当前 servlet context
 */
protected List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
determineContextInitializerClasses(ServletContext servletContext) {

    List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> classes =
            new ArrayList<>();
    //从servletContext中尝试获取globalInitializerClasses全局参数的值
    String globalClassNames = servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM);
    if (globalClassNames != null) {
        //根据",; \t\n"拆分值字符串
        for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
            //根据全路径名获取对应的Class并存入classes集合中
            classes.add(loadInitializerClass(className));
        }
    }
    //从servletContext中尝试获取contextInitializerClasses全局属性的值
    String localClassNames = servletContext.getInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM);
    if (localClassNames != null) {
        //根据",; \t\n"拆分值字符串
        for (String className : StringUtils.tokenizeToStringArray(localClassNames, INIT_PARAM_DELIMITERS)) {
            //根据全路径名获取对应的Class并存入classes集合中
            classes.add(loadInitializerClass(className));
        }
    }
    //返回最终结果集
    return classes;
}

3 总结

  本文是Spring MVC初始化源码的第一篇文章,主要讲解了两部分:

  1. 首先简单介绍了web.xml文件的加载流程,从该流程中可以找到Spring MVC项目的初始化流程。主要配置的家在顺序为:Listener – SrvletContext – listener#contextInitialized(ServletContextEvent)– Filter – filter#init(FilterConfig) – 加载load-on-startup属性大于等于0的Servlet – Servlet#init(ServletConfig)
  2. 还学习了Spring MVC的ContextLoaderListener监听器的加载流程,这个监听器实际上就是用于加载Spring MVC的根上下文容器的。主要的相关全局属性就是contextConfigLocation属性,该属性用于指定父容器Root WebApplicationContext的配置文件的位置,默认路径是/WEB-INF/applicationContext.xml,另外还介绍了一些其他步骤,比如属性源初始化。

相关文章:
  https://spring.io/
  Spring Framework 5.x 学习
  Spring MVC 5.x 学习
  Spring Framework 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

©️2021 CSDN 皮肤主题: 博客之星2020 设计师:CY__ 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值