百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

Spring 源码阅读:基于 XML 配置初始化 Spring 上下文过程总结

xsobi 2024-11-24 23:34 1 浏览

概述

最近一直在看 Spring 框架的源码,对 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 指的是给当前初始化的上下文指定的一个父上下文,这里提供了一个空值,也就是当前创建的上下文没有父上下文。

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

调用父类的构造方法

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

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

另外值得一提的是,如果在调用方法时,我们提供了 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() 方法是通过调用其他方法来完成各个步骤的,我将这些方法调用以及它们的作用罗列了出来,并根据阶段进行了分组,过程可以参考下图:

加载上下文的关键步骤

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

准备阶段

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

这里主要做了四件事:

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

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

容器创建和初始化

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

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

容器创建

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

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

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

容器预处理

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

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

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

容器后处理

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

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

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

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

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

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

组件注册

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

注册 BeanPostProcessor

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

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

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

上图是这一部分逻辑的具体流程。其中 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 类型的默认事件广播器。

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

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

onRefresh方法

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

注册事件监听器

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

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

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

收尾阶段

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

完成 BeanFactory 初始化

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

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

最后的收尾工作

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

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

最终结果

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

相关推荐

js向对象中添加元素(对象,数组) js对象里面添加元素

一、添加一个元素对象名["属性名"]=值(值:可以是一个值,可以是一个对象,也可以是一个数组)这样添加进去的元素,就是一个值或对象或数组...

JS小技巧,如何去重对象数组?(一)

大家好,关于数组对象去重的业务场景,想必大家都遇到过类似的需求吧,这对这样的需求你是怎么做的呢。下面我就先和大家分享下如果是基于对象的1个属性是怎么去重实现的。方法一:使用.filter()和....

「C/C++」之数组、vector对象和array对象的比较

数组学习过C语言的,对数组应该都不会陌生,于是这里就不再对数组进行展开介绍。模板类vector模板类vector类似于string,也是一种动态数组。能够在运行阶段设置vector对象的长度,可以在末...

如何用sessionStorage保存对象和数组

背景:在工作中,我将[{},{}]对象数组形式,存储到sessionStorage,然后ta变成了我看不懂的形式,然后我想取之用之,发现不可能了~记录这次深刻的教训。$clickCouponIndex...

JavaScript Array 对象 javascript的array对象

Array对象Array对象用于在变量中存储多个值:varcars=["Saab","Volvo","BMW"];第一个数组元素的索引值为0,第二个索引值为1,以此类推。更多有...

JavaScript中的数组Array(对象) js array数组

1:数组Array:-数组也是一个对象-数组也是用来存储数据的-和object不同,数组中可以存储一组有序的数据,-数组中存储的数据我们称其为元素(element)-数组中的每一个元素都有一...

数组和对象方法&数组去重 数组去重的5种方法前端

列举一下JavaScript数组和对象有哪些原生方法?数组:arr.concat(arr1,arr2,arrn);--合并两个或多个数组。此方法不会修改原有数组,而是返回一个新数组...

C++ 类如何定义对象数组?初始化数组?linux C++第43讲

对象数组学过C语言的读者对数组的概念应该很熟悉了。数组的元素可以是int类型的变量,例如int...

ElasticSearch第六篇:复合数据类型-数组,对象

在ElasticSearch中,使用JSON结构来存储数据,一个Key/Value对是JSON的一个字段,而Value可以是基础数据类型,也可以是数组,文档(也叫对象),或文档数组,因此,每个JSON...

第58条:区分数组对象和类数组对象

示例设想有两个不同类的API。第一个是位向量:有序的位集合varbits=newBitVector;bits.enable(4);bits.enable([1,3,8,17]);b...

八皇后问题解法(Common Lisp实现)

如何才能在一张国际象棋的棋盘上摆上八个皇后而不致使她们互相威胁呢?这个著名的问题可以方便地通过一种树搜索方法来解决。首先,我们需要写一个函数来判断棋盘上的两个皇后是否互相威协。在国际象棋中,皇后可以沿...

visual lisp修改颜色的模板函数 怎么更改visual studio的配色

(defunBF-yansemokuai(tuyuanyanse/ss)...

用中望CAD加载LISP程序技巧 中望cad2015怎么加载燕秀

1、首先请加载lisp程序,加载方法如下:在菜单栏选择工具——加载应用程序——添加,选择lisp程序然后加载,然后选择添加到启动组。2、然后是添加自定义栏以及图标,方法如下(以...

图的深度优先搜索和广度优先搜索(Common Lisp实现)

为了便于描述,本文中的图指的是下图所示的无向图。搜索指:搜索从S到F的一条路径。若存在,则以表的形式返回路径;若不存在,则返回nil。...

两个有助于理解Common Lisp宏的例子

在Lisp中,函数和数据具有相同的形式。这是Lisp语言的一个重大特色。一个Lisp函数可以分析另一个Lisp函数;甚至可以和另一个Lisp函数组成一个整体,并加以利用。Lisp的宏,是实现上述特色的...