likes
comments
collection
share

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

作者站长头像
站长
· 阅读数 44

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

概述

最近一直在看 Spring 框架的源码,并且写了一系列文章记录学习的收获,目前主要分析了 Spring 上下文的初始化和 Bean 初始化的一部分源码,文章收录在【Spring Framework 源码解读】专栏中。对 Spring 源码的阅读,可以让我更加了解一直在使用的 Spring 框架,也能让我从其中学到很多开发的技巧,比如设计模式的实践等。

随着对 Spring 源码的不断了解,对一些最开始学习到的东西,有了新的认识。因此,在继续学习之前,我打算把前面看过以及记录过的东西,做一个整体回顾。这样还有一个好处,就是之前的文章分析得相对细节,没有宏观的视角,也算是一个补充。

这篇总结一下基于 XML 配置初始化 Spring 上下文的过程,这篇文章会更多地关注流程的梳理,尽量减少代码细节的分析。

整体结构

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

上下文初始化的过程,都是从上下文类的构造方法被调用开始的。如果基于一个 XML 配置文件来初始化上下文,那么 ClassPathXmlApplicationContext 就是要用到的上下文类型。

在构造方法中,我们可以提供一个或多个 XML 配置文件的路径,上下文对象就是基于这些配置文件中的配置初始化的。

在这个构造方法中,调用了另外一个构造方法。

// 调用语句
this(new String[] {configLocation}, true, null);
// 方法定义
public ClassPathXmlApplicationContext(
      String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
      throws BeansException {
   super(parent);
   setConfigLocations(configLocations);
   if (refresh) {
      refresh();
   }
}

这个构造方法比之前多了两个参数。

  • refresh 参数传入的值是 true,因此,方法中的 refersh() 方法会被执行。
  • parent 指的是给当前初始化的上下文指定的一个父上下文,这里提供了一个空值,也就是当前创建的上下文没有父上下文。

这个构造方法中,分别调用了三个方法,下面介绍一个这三个方法分别做了什么。

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

调用父类的构造方法

首先调用父类的构造方法,执行父类构造方法的逻辑。以下是 ClassPathXmlApplicationContext 的部分继承关系:

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

父类的构造方法会逐级向上调用,在此过程中,主要是初始化了一些成员变量的值,其中包括上下文中包含的类加载器、资源加载器、资源模式解析器等,以及一些跟上下文配置相关的默认值。这些都是为之后的初始化做准备。

另外值得一提的是,如果在调用方法时,我们提供了 parent,也就是父上下文,在这部分还会对当前上下文和父上下文的 Environment (环境抽象)做合并。

至此,执行完这一步,上下文还是一个空架子。

设置配置文件路径

因为 ClassPathXmlApplicationContext 是一个基于 XML 配置的上下文对象,因此,在执行完父类的构造逻辑之后,就要开始处理 XML 配置了。

setConfigLocations 方法看起来像是一个普通的 Setter 方法,作用是把构造方法参数 configLocations 赋值给上下文中对应的成员变量。这个方法的作用确实如此,但是会对 XML 文件路径做一些处理,过程中还会初始化上下文中的一些其他组件。

这是因为,这里提供的配置文件路径是可以包含占位符的,比如:classpath:config/beans-${env}.xml

此时,Spring 需要根据配置信息将这里的 ${env} 替换成一个实际对值。在此过程中会涉及到 Spring 上下文中的多个组件:

  • Environment 环境抽象。如果当前上下文中还没有这个对象,则会创建一个默认的 StandardEnvironment,这是对程序当前运行环境的一个抽象。
  • PropertySources 参数来源。这里需要 Environment 的原因就是,它里面会包含 PropertySources。其中包含多个 PropertySource 参数来源。前面提到 Spring 会将配置文件路径中的占位符替换成对应的值,就是从这些参数来源来查找的。默认情况下,Spring 会添加两个参数来源,分别是系统运行参数和系统环境变量。
  • PropertyResolver 参数解析。有了参数的来源,Environment 在创建时还会初始化一个 PropertyResolver 负责参数的解析和处理。

有了以上这些,上下文中的 configLocations 成员变量所保存的就是占位符已经被处理替换过的真实文件路径。在此过程中创建的参数处理相关的对象,也被包含在上下文中,之后也会用到。

刷新上下文(核心流程)

构造方法的最后就是执行了 refresh() 方法,虽然方法的名字直译是刷新的意思,但它其实是加载整个上下文的过程,刷新操作也是重新执行一次加载的过程。这个方法包含了上下文初始化的所有逻辑。

在源码中,refresh() 方法是通过调用其他方法来完成各个步骤的,我将这些方法调用以及它们的作用罗列了出来,并根据阶段进行了分组,过程可以参考下图:

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

加载上下文的关键步骤

下面总结各个环节主要完成了哪些上下文初始化的工作。

准备阶段

首先是在加载之前对上下文进行了预处理。

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

这里主要做了四件事:

  • 设置了几个上下文的状态属性。
  • 执行了初始化参数源的方法。这个方法是 protected 修饰的,且方法体为空,是作为一个扩展点提供给子类的。
  • 验证启动上下文时必要的参数是否完整。
  • 初始化保存事件监听器和事件的集合变量。

可以看到这部分的工作十分简单,主要是为后续的流程做准备。

容器创建和初始化

接下来开始 BeanFactory 的创建,BeanFactory 是上下文内部的 Bean 容器,也是 Spring 的底层容器。与之相对的,ApplicationContext 也可以被称为高级容器,它继承了 BeanFactory 并且做了很多功能扩展,使之不仅仅被作为容器来使用。在容器创建完之后执行的操作,本质上都是对底层容器的扩展。

先从容器的创建开始看起。

容器创建

容器对象创建和初始化的流程总结如下:

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

这里可以看到,默认情况下创建的容器对象是 DefaultListableBeanFactory 类型,创建过程中,还添加了几个在属性注入时需要忽略的属性对应的感知接口,其余的一些属性初始化,它们的值都与容器所属的当前上下文对象的同名属性值保持一致。

其中,有一个步骤值得特别注意,就是调用 loadBeanDefinitions 方法加载 BeanDefinition 这一步。图中没有详细画出这一步所做的具体工作,是因为这一步的流程特别长,它包括 XML 配置文件资源的加载、内容验证和解析、BeanDefinition 的创建和注册,以及其他一些相关的流程。想要了解具体的过程,可以参考我之前的源码分析文章。在【Spring Framework 源码解读】专栏的第 6 到 9 篇,四篇文章加起来差不多一万字了。

容器预处理

有了容器对象,并且注册好了 BeanDefinition 后,接下来 Spring 上下文会对容器进行预处理。

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

这里的流程比较多,都是一些相互之间关系不是很大的容器预处理。我挑几个比较值得关注的来介绍一下。

  • 第一个比较值得关注的是 BeanExpressionResolver 的初始化,这里在容器中设置了一个表达式解析器,在初始化解析器时,还会创建 SpEL 的解析和配置对象。这部分主要提供了解析 SpEL 表达式的处理能力。
  • 第二个是 ApplicationListenerDetector 的注册,也就是向容器中注册了一个 Bean 后处理器,后处理器是在 Bean 被初始化的时候才发挥作用的,那 Spring 为什么在这个阶段就要将这个后处理器注册到容器中呢?这是因为有一些 Spring 内部的 Bean 在接下来要陆续开始注册了,这些 Bean 有可能会需要这个后处理器来处理,它的作用很简单,就是在 Bean 初始化完成之后进行判断,如果这个 Bean 实现了 ApplicationListener 接口,则将它注册为容器的事件监听器。这个后处理器会在上下文整个生命周期中发挥作用,因此,如果我们要开发一个事件监听器,只需要实现这个接口就可以了,Spring 上下文会自动将它注册为事件监听器到容器中。
  • 最后一个值得注意的是流程图里的倒数第四个步骤,注册了几个指定接口和实现类的 Bean,这里主要保证了这几个接口或类的 Bean,只能是此处指定的对象,是的容器中几个重要的基础的 Bean 不会被篡改。

容器后处理

接下来是针对 BeanFactory 的后处理操作。

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

这里包含的两个步骤中,第一个方法 postProcessBeanFactory 是一个扩展点,留给子类实现逻辑。

然后就是执行所有的 BeanFactoryPostProcessor 后处理器,这里可以对初始化完的 BeanFactory 进行操作。这里涉及到了两类后处理器:

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

BeanFactoryPostProcessor 中定义了 postProcessBeanFactory 方法,在它的子接口 BeanDefinitionRegistryPostProcessor 中增加了 postProcessBeanDefinitionRegistry 方法的定义。这两个方法都会这当前阶段被调用,后者会先被调用。

这一部分的任务,会被委派给 PostProcessorRegistrationDelegate,调用方法时,会从当前的上下文中获取已经注册的所有 BeanFactoryPostProcessor 作为参数传入,并且,在方法体内,也会从容器中获取到所有没有被注册为 BeanFactoryPostProcessor 但是实现了 BeanFactoryPostProcessor 接口的 Bean,它们的后处理逻辑也会被执行。

具体的执行过程,参考下面的流程图,可以同时阅读我之前的源码分析文章【Spring 源码阅读 13:执行 BeanFactoryPostProcessor 中的处理方法 】:

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

组件注册

容器初始化好了,接下来进入到组件注册的阶段,这也是 ApplicationContext 对底层容器扩展的主要部分。

注册 BeanPostProcessor

BeanPostProcessor 是对 Bean 的初始化过程的扩展,BeanPostProcessor 接口以及它的子接口定义的方法,会在 Bean 实例的创建、属性注入、初始化方法执行等阶段阶段被调用。以下是几个常见的 BeanPostProcessor 接口:

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

在 Spring 上下文的 BeanFactory 创建和初始化完成之后,Spring 会将容器中已经注册的实现了 BeanPostProcessor 机器子接口的 Bean 注册,并在实例化 Bean 的阶段调用它们的处理方法。这部分逻辑,被委派给了 PostProcessorRegistrationDelegate 类。

注册 BeanPostProcessor 的逻辑,跟执行初始化 BeanFactory 阶段中执行 BeanFactoryPostProcessor 的逻辑很相似。Spring 会从容器中获取到已经被注册的实现了 BeanFactoryPostProcessor 的 Bean 名称,然后根据他们是否实现了 PriorityOrdered 接口和 Ordered 接口进行分组,再将每一个分组中BeanFactoryPostProcessor 进行排序,依次注册。这里的注册顺序会影响执行的顺序。

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

上图是这一部分逻辑的具体流程。其中 BeanPostProcessorChecker 在整个过程的开头和结尾各注册过一次,这个后处理器的作用是,在后处理器注册的过程中,如果有 Bean 的实例正在被初始化,那么会记录日志,表示这个 Bean 初始化的过程中并没有被所有的后处理器处理过(因为此时后处理器还没有全部注册)。

另外,在 Spring 注册后处理器的逻辑中,如果一个后处理器已经被处理过,那么,之前注册的会被移除,然后再注册在后处理器列表的末尾,因此,重新注册一个后处理器也有调整顺序的作用。

这里还有一点要注意的是,这里只是将 BeanPostProcessor 注册到容器中,并没有执行其中的逻辑,这些逻辑在 Bean 实例初始化的过程的特定阶段中才会被执行。

初始化 MessageSource

MessageSource 跟整个流程中的其他部分几乎没有紧密的关系,它是 Spring 框架中内置的 i18n 组件,Spring 会从容器中查找名称为 messageSource、类型为 MessageSource 的 BeanDefinition,如果存在的话,则会通过容器的 getBean 方法获取到初始化后的实例,并作为 MessageSource 赋值给上下文的对应成员变量。如果容器中没有这个 BeanDefinition,Spring 会创建一个默认的 DelegatingMessageSource 类型的 MessageSource。

注意,这里可以看到,如果要自定义 MessageSource,只能以名称 messageSource 作为名称注册到容器中,否则将不会被 Spring 采用。

初始化组件广播器

事件广播器的初始化逻辑跟 MessageSource 的初始化逻辑相同,Spring 会在容器中查找名称为 applicationEventMulticaster 且类型为 ApplicationEventMulticaster 的 BeanDefinition,如果存在,则获取实例作为当前上下文的事件广播器,如果不存在,则会创建一个 SimpleApplicationEventMulticaster 类型的默认事件广播器。

以下是事件广播器的工作原理:

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

事件发布者通过调用上下文对象的 publishEvent 方法发布一个事件,然后上下文对象会通过内部的事件广播器,找到已经注册的所有事件监听器,对于能够处理当前事件的所有事件监听器,调用它们相应的处理方法来处理该事件,相当于一个 Spring 内置的 Pub/Sub 模型。

onRefresh方法

onRefresh 方法是一个 protected 修饰的空方法,说明这里也是一个扩展点。此时 Spring 完成了容器的初始化,以及特殊 Bean 的初始化和注册,但是还没有注册一般的 Bean 实例。子类可以通过充血这个方法来实现一些需要在此时执行的逻辑。

注册事件监听器

前面介绍了事件广播器,对于事件发布,只要通过上下文对象调用方法即可将事件发布,但是对于事件监听器,它们需要作为一类 Bean 单独进行注册。在 Spring 中,作为事件监听器的 Bean 需要实现 ApplicationListener 接口,并在 onApplicationEvent 方法中实现事件处理逻辑。

事件监听器的注册逻辑如下:

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

这里有一个值得注意的地方是,在事件监听器被注册之前,可能就有事件被发布,这些早期发布的事件被缓存在 earlyApplicationEvents 集合中,当事件监听器注册完成后,会通过事件广播器广播这些事件,从而让事件监听器可以在被注册后第一时间处理这些事件。

收尾阶段

这一阶段的两个方法调用,都是处理上下文初始化收尾的工作,第一个 finishBeanFactoryInitialization 方法处理了 BeanFactory 相关的收尾工作,而 finishRefresh 则是整个 refresh 方法的收尾工作。

完成 BeanFactory 初始化

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

上图是这一阶段的流程图,比较琐碎,其中最重要的一个部分就是最后的一个步骤,也就是,将非懒加载的单例 Bean 初始化,这一步的好处是,在之后需要获取这些 Bean 实例的时候,可以直接获取到对象。

这里主要的步骤是调用 getBean 方法,也就是容器中最常用的获取 Bean 实例的方法。

最后的收尾工作

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

在最后阶段,最主要的步骤是初始化了上下文生命周期的处理器,以及发布了上下文加载完成的事件。

上下文生命周期处理器可以在上下文启动和停止的时候,调用实现了 Lifecycle 接口的 Bean 的相应方法,可以让这些 Bean 在容器启动和停止时执行一些逻辑。

Spring 源码阅读 29:基于 XML 配置初始化 Spring 上下文过程总结(10+详细流程图)

最终结果

最终,完成上下文初始化之后,就得到了一个 ClassPathXmlApplicationContext 类型的上下文对象。其中包含了一个内置的 DefaultListableBeanFactory 类型的 BeanFactory 容器,以及 MessageSource、事件广播器等组件。BeanFactory 中还包含了所有的 BeanDefinition、已经被初始化的 Bean 实例、后处理器等。简而言之,这个上下文对象中,已经初始化好了之后需要用到的所有组件。