小白都能看懂的Spring源码揭秘之IOC容器源码分析
前言
在 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 配置文件的过程改为解析注解的过程,核心的后续流程其实还是一样。
小白都能看懂的Spring源码揭秘之IOC容器源码分析的更多相关文章
- 小白都能看懂的 Spring 源码揭秘之依赖注入(DI)源码分析
目录 前言 依赖注入的入口方法 依赖注入流程分析 AbstractBeanFactory#getBean AbstractBeanFactory#doGetBean AbstractAutowireC ...
- 小白都能看懂的 Spring 源码揭秘之Spring MVC
目录 前言 Spring MVC 请求流程 Spring MVC 两大阶段 初始化 HttpServletBean#init() FrameworkServlet#initServletBean Fr ...
- 搭建分布式事务组件 seata 的Server 端和Client 端详解(小白都能看懂)
一,server 端的存储模式为:Server 端 存 储 模 式 (store-mode) 支 持 三 种 : file: ( 默 认 ) 单 机 模 式 , 全 局 事 务 会 话 信 息 内 存 ...
- 小白都能看懂的tcp三次握手
众所周知,TCP在建立连接时需要经过三次握手.许多初学者经常对这个过程感到混乱:SYN是干什么的,怎么一会儿是1一会儿是0?怎么既有大写的ACK又有小写的ack?为什么ACK在第二次握手才开始出现?初 ...
- Spring Cloud Alibaba分布式事务组件 seata 详解(小白都能看懂)
一,什么是事务(本地事务)? 指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行. 简单的说,事务就是并发控制的单位,是用户定义的一个操作序列. 而一个逻辑工作单元要成 ...
- log4j漏洞的产生原因和解决方案,小白都能看懂!!!!
核弹级bug Log4j,相信很多人都有所耳闻了,这两天很多读者都在问我关于这个bug的原理等一些问题,今天咱们就专门写一篇文章,一起聊一聊这个核弹级别的bug的产生原理以及怎么防止 产生原因 其实这 ...
- Unity 打包发布Android新手教学 (小白都能看懂的教学 ) [转]
版权声明:本文为Aries原创文章,转载请标明出处.如有不足之处欢迎提出意见或建议,联系QQ531193915 扫码关注微信公众号,获取最新资源 最近在Unity的有些交流群里,发现好多Unity开发 ...
- 小白都能看懂的Linux系统下安装配置Zabbix
实验环境: 操作系统:Centos 7.6 服务器ip:192.168.10.100 运行用户:root 网络环境:Internet Zabbix是一个基于web界面的提供分布式系统监控及网络功能的企 ...
- gitbook 入门教程之小白都能看懂的 Gitbook 插件开发全流程
什么是插件 Gitbook 插件是扩展 GitBook 功能(电子书和网站)的最佳方式. 只要是 Gitbook 默认没有提供的功能,基于插件机制都可以自行扩展,是插件让 Gitbook 变得更加强大 ...
随机推荐
- php 日期相关的类 DateInterval DateTimeZone DatePeriod
* DateInterval <?php /** * Created by PhpStorm. * User: Mch * Date: 7/18/18 * Time: 21:30 */ $dat ...
- linux 上添加多个jdk
1. 首先将你需要上传的jdk 上传并解压 2.你可以自定义解压的路径 3. alternatives --install /usr/bin/java java /usr/java/jdk1.7.0_ ...
- lua文件修改为二进制文件
注意:lua编译跟luajit编译的二进制文件是不兼容,不能运行的 如果是使用luajit,请直接使用luajit直接编译二进制 第一种:luajit编译(以openresty为例,跟luac是相反的 ...
- 鸿蒙内核源码分析(根文件系统) | 先挂到`/`上的文件系统 | 百篇博客分析OpenHarmony源码 | v66.01
百篇博客系列篇.本篇为: v66.xx 鸿蒙内核源码分析(根文件系统) | 先挂到/上的文件系统 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...
- 华为云计算IE面试笔记-华为云计算解决方案业务迁移支持哪些迁移?有哪些特点?请描述基本的业务交付流程、业务迁移流程和原则。
1. 迁移场景:华为云计算解决方案按照源端环境来说,支持P2V.V2V(P2V:物理设备(操作系统及其上的应用软件和数据)迁移到华为虚拟化平台.V2V:其他厂商的虚拟化平台迁移到华为虚拟化平台.)以及 ...
- GitHub 和 Gitee 开源免费 10 个超赞后台管理面板,看完惊呆了!
软件工程师在实际项目开发中不可避免需要依赖一些前后端的后台管理系统框架,而不是从零开始一点点的搭建,浪费人力.目前市面上有很多开放源码.且免费的后台管理面板,样式色彩也比较丰富美观. 今天整理 ...
- c++ class里面成员和分配内存问题
慢慢开始学c++啦,记录学习的大体过程 class中神奇的内存(sizeof) 1.内存补齐 便于管理类(生成的对象)的内存,类总内存总是为最大成员字节大小的倍数,不足的会进行内存补齐 类的整体内存就 ...
- 日常学习用到的Git指令
Git 常用Git指令 (本地) git init - 将文件夹初始化为Git仓库 git add - 将工作区的指定文件放入暂存区 git status - 查看工作区和暂存区的状态 git com ...
- WSL (Windows Subsystem for Linux)
WSL (Windows Subsystem for Linux) :适用于 Linux 的 Windows 子系统. References Install WSL with a single com ...
- C#特性知识图谱-二、事件
C#特性知识图谱-二.事件 二.事件 在事件驱动的软件系统中,符合某种预设条件的情形出现是,一个事件就会被触发. 2.1 事件三要素 事件源:激发事件的对象 事件信息:事件本身说携带的信息 事件响应者 ...