前言
在 Spring
框架中,大家耳熟能详的无非就是 IOC
,DI
,Spring MVC
,AOP
,这些是 Spring
中最基础的核心功能,再高级点的功能就还有数据数据访问模块(JDBC
,ORM
,事务等)。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:定义了一些读取配置文件的方法,支持使用
Resource
和String
位置参数指定加载方法,具体的时候可以扩展自己的特有方法。该类只是提供了一个建议标准,不要求所有的解析都实现这个接口。 - BeanFactory:访问
Bean
容器的顶层接口,我们最常用的ApplicationContext
接口也实现了BeanFactory
。
IOC 初始化三大步骤
上面我们大致知道了 IOC
容器是什么,也知道了 IOC
容器用来存储什么,同时也对 IOC
的核心三大接口混了个眼熟,那么接下来我们就该了解下 Bean
到底是怎么来的,存到 IOC
容器的又只是 Bean
本身还是做了进一步封装呢?
带着这两个问题就让我们来细细分析一下 IOC
的整个初始化流程。
IOC
的整个初始化流程可以概要的分为三大步骤:定位,加载,注册。
- 定位:寻找需要初始化哪些
Bean
。 - 加载:将寻找到需要初始化的
Bean
进行解析封装。 - 注册:这一步就是将第二步加载后的
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
解析默认节点方法:
在这里又分为了不同情况去解析 import
,alias
,bean
节点,也包括了嵌套节点的递归处理方式,我们继续进入 processBeanDefinition
方法:
到这里基本上就要结束注册流程了,调用了 BeanDefinitionReaderUtils
工具类中的一个方法来进行注册:
在这里做了三件事:
- 获取到
beanName
。 - 回到最开始的
DefaultListableBeanFactory
,调用registerBeanDefinition
方法 - 存在别名的话注册一下别名。
在这里最关键的是第二步,我们发现绕了一大圈最终回到了我们前面加载步骤中的 DefaultListableBeanFactory
类(下面这个方法我为了方便截屏,删除了部分的异常判断):
这个方法就是注册 bean
的最后逻辑,首先会判断当前 bean
是否已经被注册,有的话会判断是否允许覆盖之类的一些设置,如果最终都能符合条件,那么就会直接覆盖(795
行),如果当前 bean
是首次创建,那么还需要判断当前整个 ioc
容器是否已经有创建好的 bean
,但是最终其实就是 this.beanDefinitionMap.put(beanName, beanDefinition);
这行代码完成了注册,而 beanDefinitionMap
其实就是一个 ConcurrentHashMap
集合。
到这里我们整个 ioc
加载主流程就分析结束了,其实整个逻辑非常简单,而我们之所以会觉得 Spring
复杂难懂,其实是因为 Spring
为了扩展性,可读性,经过了精心设计,整个框架中使用了非常多的设计模式和设计原则,致使我们看源码的时候觉得非常绕,但是只要抓住核心主干,读懂源码也并不是难事。
总结
本文主要讲述了 ioc
的初始化流程,整个过程其实是非常绕非常复杂的,第一次看的话非常容易绕迷路,所以我们需要抓住主流程,理解 ioc
的核心就是三个步骤:定位(找配置文件),加载(解析配置文件),注册(将 bean
添加到 ioc
容器)非常关键,只要抓住这三个步骤,我们就能抓住重点一步步往下跟。所以如果我们把获取 bean
的方式换成注解实现,无非就是把解析 xml
配置文件的过程改为解析注解的过程,核心的后续流程其实还是一样。