小白都能看懂的Spring源码揭秘之IOC容器源码分析

前言

Spring 框架中,大家耳熟能详的无非就是 IOCDISpring MVCAOP,这些是 Spring 中最基础的核心功能,再高级点的功能就还有数据数据访问模块(JDBCORM,事务等)。Spring 本身的扩展性也做得非常好,源码当中也是运用了大量设计模式来实现,了解 Spring 源码对于一个 Java 开发人员来说是非常有必要的,从源码中我们也可以学习到很多优秀的设计理念,现在就让我们从 Spring IOC 开启 Spring 源码之旅吧。

IOC 只是一个 Map 集合

提到 IOC,初次接触的人可能会觉得非常高大上,觉得是一种很高深的技术,然而事实呢?事实是 IOC 其实仅仅只是一个 Map 集合而已,并不是什么高深的新技术,请各位大佬们坐下喝杯茶听我细细道来。

IOC 全称为:Inversion of Control。控制反转的基本概念是:不用创建对象,但是需要描述创建对象的方式。

简单的说我们本来在代码中创建一个对象是通过 new 关键字,而使用了 Spring 之后,我们不在需要自己去 new 一个对象了,而是直接通过容器里面去取出来,再将其自动注入到我们需要的对象之中,即:依赖注入。

也就说创建对象的控制权不在我们程序员手上了,全部交由 Spring 进行管理,程序要只需要注入就可以了,所以才称之为控制反转。

实际上,IOC 也被称之为 IOC 容器,那么既然是一个容器,肯定是要用来放东西的,那么 IOC 容器用来存储什么呢?如果大家对 Spring 有所了解的话,那就知道在 Spring 里面可以说是一切面向 Bean 编程,而 Bean 指的就是我们交给 Spring 管理的对象,今天我们要学习的 IOC 容器就是用来存储所有 Bean 的一个容器。

IOC 三大核心接口

Spring 作为一款优秀的框架,对于 Bean 的来源也支持很多种,那么为了统一标准,自然需要定义一个配置文件接口,这就是 BeanDefinition;有了配置标准,那就要定义相关的类来将不同的配置文件进行转换,所以就有了 BeanDefinitionReader;最终将 Bean 解析完成之后,那么还需要对 Bean 进行操作,于是又有了 BeanFactory。这三个接口就构成了 IOC 的核心:

  • BeanDefinition:定义了一个 Bean 相关配置文件的各种信息,比如当前 Bean 的构造器参数,属性,以及其他一些信息,这个接口同样也会衍生出其他一些实现类,如
  • BeanDefinitionReader:定义了一些读取配置文件的方法,支持使用 ResourceString 位置参数指定加载方法,具体的时候可以扩展自己的特有方法。该类只是提供了一个建议标准,不要求所有的解析都实现这个接口。
  • BeanFactory:访问 Bean 容器的顶层接口,我们最常用的 ApplicationContext 接口也实现了 BeanFactory

IOC 初始化三大步骤

上面我们大致知道了 IOC 容器是什么,也知道了 IOC 容器用来存储什么,同时也对 IOC 的核心三大接口混了个眼熟,那么接下来我们就该了解下 Bean 到底是怎么来的,存到 IOC 容器的又只是 Bean 本身还是做了进一步封装呢?

带着这两个问题就让我们来细细分析一下 IOC 的整个初始化流程。

IOC 的整个初始化流程可以概要的分为三大步骤:定位加载注册

  1. 定位:寻找需要初始化哪些 Bean
  2. 加载:将寻找到需要初始化的 Bean 进行解析封装。
  3. 注册:这一步就是将第二步加载后的 Bean 放入 IOC 容器,也就是放入 Map 集合之中。

定位

我们最常用的 Bean 一般来源于 xml 配置或者注解,那么这些配置文件又存储在哪里呢? 在 Spring 中配置文件支持以下六种来源:

  • classpath
  • network
  • filesystem
  • servletContext
  • annotation

接下来我们以我们最常用的一种方式作为入口来分析一下定位的流程(ApplicationContext 实现的顶层接口之一就是 BeanFactory,所以其具有 BeanFactory 的操作 Bean 的能力):

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
applicationContext.getBean("myBean");
applicationContext.getBean(MyBean.class);

在以前使用传统 Spring 的时候,我们就是通过上面这种方式来获取 Bean,定位的入口我们就从 ClassPathXmlApplicationContext 的入口开始吧。

这里的逻辑非常简单,先调用 setConfigLocations 方法设置配置文件,然后核心就在 refresh 方法,refresh 是其父类实现的,而父类中的 refresh 方法的主干就是在 522 行获取一个 beanFactory,后面的所有操作都是围绕 beanFactory 做一些扩展操作。

其实看 522 行的注释也可以知道,最终其还是会调用回子类也就是 AbstractRefreshableApplicationContext 来执行加载 bean 操作:

这里面需要说明的是,核心逻辑是在 623 行,而 624 行实际上是从全局变量内获取 beanFactory

而这里的全局变量 beanFactory 就是 BeanFactory 的一个默认实现 DefaultListableBeanFactory。了解了这个之后,我们继续回到上面的 refreshBeanFactory

这个方法其实也很简单,就是创建了一个默认的 DefaultListableBeanFactory,然后就开始调用其子类 AbstractXmlApplicationContext(同时其是 ClassPathXmlApplicationContext 父类)的 loadBeanDefinitions 方法:

加载

执行到上面的方法中,我们可以发现到一个 BeanDefinitionReader 对象 XmlBeanDefinitionReader 被创建了,这就说明到这里差不多要开始加载配置文件了,所以接下来要找主干其实只要跟着这个 BeanDefinitionReader 对象就可以了,我们继续进入 loadBeanDefinitions 方法:

这里面分为了两种情况,一种是根据 Resource 类型,一种是根据 String 类型,我们这里因为传的是一个 String 类型的路径,所以会执行下面的逻辑,但是虽然执行的是下面的逻辑,但是最终还是会将我们传入的 spring.xml 转化成 Resource,从而调用上面的解析方法。

接下来还会经过几次“绕路”,然后还是会进入 XmlBeanDefinitionReader 对象的 loadBeanDefinitions 方法:

在这里我们终于看到了一个令我们惊喜的方法 doLoadBeanDefinitions,因为在 Spring 当中,基本上以 do 开头的方法就是真正的核心处理逻辑方法:

这里面就是调用了两个方法,第一个就是把 resource 转化成 document 对象,然后调用另一个方法准备注册 bean,当然怎么解析我们的 xml 配置文件,我们在这里不做分析,继续看主干注册 bean 的逻辑。

注册

上面调用注册方法之后,最终会由其子类 DefaultBeanDefinitionDocumentReader 来执行:

到这里我们又开到了以 do 开头的方法,说明这里要开始注册了。

这里创建了一个委派者 delegate,进入这个委派者我们可以发现,这里面定义了 xml 文件中的所有节点:

创建好委派者之后,接下来就可以开始调用 parseBeanDefinitions 来进行解析了:

到这里又分成了三种情况,是否默认命名空间以及是否默认节点,但是不管是什么情况,最终都是会把节点信息解析出来转换成一个 bean 进行注册,我们进入 parseDefaultElement 解析默认节点方法:

在这里又分为了不同情况去解析 importaliasbean 节点,也包括了嵌套节点的递归处理方式,我们继续进入 processBeanDefinition 方法:

到这里基本上就要结束注册流程了,调用了 BeanDefinitionReaderUtils 工具类中的一个方法来进行注册:

在这里做了三件事:

  1. 获取到 beanName
  2. 回到最开始的 DefaultListableBeanFactory,调用 registerBeanDefinition 方法
  3. 存在别名的话注册一下别名。

在这里最关键的是第二步,我们发现绕了一大圈最终回到了我们前面加载步骤中的 DefaultListableBeanFactory 类(下面这个方法我为了方便截屏,删除了部分的异常判断):

这个方法就是注册 bean 的最后逻辑,首先会判断当前 bean 是否已经被注册,有的话会判断是否允许覆盖之类的一些设置,如果最终都能符合条件,那么就会直接覆盖(795 行),如果当前 bean 是首次创建,那么还需要判断当前整个 ioc 容器是否已经有创建好的 bean,但是最终其实就是 this.beanDefinitionMap.put(beanName, beanDefinition); 这行代码完成了注册,而 beanDefinitionMap 其实就是一个 ConcurrentHashMap 集合。

到这里我们整个 ioc 加载主流程就分析结束了,其实整个逻辑非常简单,而我们之所以会觉得 Spring 复杂难懂,其实是因为 Spring 为了扩展性,可读性,经过了精心设计,整个框架中使用了非常多的设计模式和设计原则,致使我们看源码的时候觉得非常绕,但是只要抓住核心主干,读懂源码也并不是难事。

总结

本文主要讲述了 ioc 的初始化流程,整个过程其实是非常绕非常复杂的,第一次看的话非常容易绕迷路,所以我们需要抓住主流程,理解 ioc 的核心就是三个步骤:定位(找配置文件),加载(解析配置文件),注册(将 bean 添加到 ioc 容器)非常关键,只要抓住这三个步骤,我们就能抓住重点一步步往下跟。所以如果我们把获取 bean 的方式换成注解实现,无非就是把解析 xml 配置文件的过程改为解析注解的过程,核心的后续流程其实还是一样。

Tags: