在使用spring
框架的日常开发中,bean
之间的循环依赖太频繁了,spring
已经帮我们去解决循环依赖问题,对我们开发者来说是无感知的,下面具体分析一下spring
是如何解决bean之间循环依赖,为什么要使用到三级缓存,而不是二级缓存?
1 bean生命周期
首先大家需要了解一下bean
在spring
中的生命周期,bean
在spring
的加载流程,才能够更加清晰知道spring
是如何解决循环依赖的。
Spring IOC
中Bean
的生命周期大致分为四个阶段:
- 实例化(
Instantiation
) - 属性赋值(
Populate
) - 初始化(
Initialization
) - 销毁(
Destruction
)
1.1 bean生命周期中重要接口
在这里插入图片描述我们在spring
的BeanFactory
工厂列举了很多接口,代表着bean
的生命周期,我们主要记住的是圈红线圈出来的接口, 再结合spring
的源码来看这些接口主要是在哪里调用的
1.2 创建bean
在这里插入图片描述AbstractAutowireCapableBeanFactory
类的doCreateBean
方法是创建bean
的开始,我们可以看到首先需要实例化这个bean
,也就是在堆中开辟一块内存空间给这个对象,createBeanInstance
方法里面逻辑大概就是采用反射生成实例对象,进行到这里表示对象还并未进行属性的填充,也就是@Autowired
注解的属性还未得到注入
1.2.1 两个阶段
Spring
将管理的一个个的依赖对象称之为Bean
,这从xml
配置文件中也可以看出。
Spring IOC
容器就好像一个生产产品的流水线上的机器,Spring
创建出来的Bean
就好像是流水线的终点生产出来的一个个精美绝伦的产品。既然是机器,总要先启动,Spring也不例外。因此Bean的一生从总体上来说可以分为两个阶段:
- 容器启动阶段
- Bean实例化阶段
容器的启动阶段做了很多的预热工作,为后面Bean
的实例化做好了充分的准备,我们首先看一下容器的启动阶段都做了哪些预热工作。
1.2.2 容器启动阶段
1.2.2.1 配置元信息
我们说Spring IOC
容器将对象实例的创建与对象实例的使用分离,我们的业务中需要依赖哪个对象不再依靠我们自己手动创建,只要向Spring
要,Spring
就会以注入的方式交给我们需要的依赖对象。
既然我们将对象创建的任务交给了Spring
,那么Spring
就需要知道创建一个对象所需要的一些必要的信息。而这些必要的信息可以是Spring
过去支持最完善的xml
配置文件,或者是其他形式的例如properties
的磁盘文件,也可以是现在主流的注解,甚至是直接的代码硬编码。总之,这些创建对象所需要的必要信息称为配置元信息。
<bean id="role" class="com.wbg.springxmlbean.entity.Role">
<!-- property元素是定义类的属性,name属性定义的是属性名称 value是值
相当于:
Role role=new Role();
role.setId(1);
role.setRoleName("高级工程师");
role.setNote("重要人员");-->
<property name="id" value="1"/>
<property name="roleName" value="高级工程师"/>
<property name="note" value="重要人员"/>
</bean>
1.2.2.2 BeanDefination
我们大家都知道,在Java
世界中,万物皆对象,散落于程序代码各处的注解以及保存在磁盘上的xml或者其他文件等等配置元信息,在内存中总要以一种对象的形式表示,就好比我们活生生的人对应到Java
世界中就是一个Person
类。
而Spring
选择在内存中表示这些配置元信息的方式就是BeanDefination
,这里我们只是需要知道配置元信息被加载到内存之后是以BeanDefination
的形存在的
1.2.2.3 BeanDefinationReader
大家肯定很好奇,我们是看得懂Spring
中xml
配置文件中一个个的Bean
定义,但是Spring
是如何看懂这些配置元信息的呢?这个就要靠我们的BeanDefinationReader
了。
不同的BeanDefinationReader
就像葫芦兄弟一样,各自拥有各自的本领。如果我们要读取xml
配置元信息,那么可以使用XmlBeanDefinationReader
。如果我们要读取properties
配置文件,那么可以使用PropertiesBeanDefinitionReader
加载。而如果我们要读取注解配置元信息,那么可以使用 AnnotatedBeanDefinitionReader
加载。我们也可以很方便的自定义BeanDefinationReader
来自己控制配置元信息的加载。假如我们的配置元信息现有的不能满足,那么我们可以自定义From BeanDefinationReader
总的来说,BeanDefinationReader
的作用就是加载配置元信息,并将其转化为内存形式的BeanDefination
,存在某一个地方
1.2.2.4 BeanDefinationRegistry
执行到这里,总算不遗余力的将存在于各处的配置元信息加载到内存,并转化为BeanDefination
的形式,这样我们需要创建某一个对象实例的时候,找到相应的BeanDefination
然后创建对象即可。那么我们需要某一个对象的时候,去哪里找到对应的BeanDefination
呢?
这种通过Bean
定义的id
找到对象的BeanDefination
的对应关系或者说映射关系又是如何保存的呢?这就引出了BeanDefinationRegistry
了。
Spring
通过BeanDefinationReader
将配置元信息加载到内存生成相应的BeanDefination
之后,就将其注册到BeanDefinationRegistry
中,BeanDefinationRegistry
就是一个存放BeanDefination
的大篮子,它也是一种键值对的形式,通过特定的Bean
定义的id
,映射到相应的BeanDefination
。
1.2.2.5 BeanFactoryPostProcessor
BeanFactoryPostProcessor
是容器启动阶段Spring
提供的一个扩展点,主要负责对注册到BeanDefinationRegistry
中的一个个的BeanDefination
进行一定程度上的修改与替换。
例如我们的配置元信息中有些可能会修改的配置信息散落到各处,不够灵活,修改相应配置的时候比较麻烦,这时我们可以使用占位符的方式来配置。例如配置Jdbc的DataSource连接的时候可以这样配置:
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="maxIdle" value="${jdbc.maxIdle}"></property>
<property name="maxActive" value="${jdbc.maxActive}"></property>
<property name="maxWait" value="${jdbc.maxWait}"></property>
<property name="minIdle" value="${jdbc.minIdle}"></property>
<property name="driverClassName"
value="${jdbc.driverClassName}">
</property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
BeanFactoryPostProcessor
就会对注册到BeanDefinationRegistry
中的BeanDefination
做最后的修改,替换$
占位符为配置文件中的真实的数据。
至此,整个容器启动阶段就算完成了,容器的启动阶段的最终产物就是注册到BeanDefinationRegistry
中的一个个BeanDefination
了,这就是Spring为
Bean实例化`所做的预热的工作。让我们再通过一张图的形式回顾一下容器启动阶段都是搞了什么事吧。
1.2.3 Bean实例化阶段
需要指出,容器启动阶段与Bean
实例化阶段存在多少时间差,如果我们选择懒加载的方式,那么直到我们伸手向Spring
要依赖对象实例之前,其都是以BeanDefinationRegistry
中的一个个的BeanDefination
的形式存在,也就是Spring
只有在我们需要依赖对象的时候才开启相应对象的实例化阶段。
而如果我们不是选择懒加载的方式,容器启动阶段完成之后,将立即启动Bean
实例化阶段,通过隐式的调用所有依赖对象的getBean
方法来实例化所有配置的Bean
并保存起来。
1.2.3.1 对象创建策略
对象的创建采用了策略模式
,借助我们前面BeanDefinationRegistry
中的BeanDefination
,我们可以使用反射的方式创建对象,也可以使用CGlib
字节码生成创建对象。
同时我们可以灵活的配置来告诉Spring
采用什么样的策略创建指定的依赖对象。Spring中Bean
的创建是策略设计模式的经典应用。这个时候,内存中应该已经有一个我们想要的具体的依赖对象的实例了,但是故事的发展还没有我们想象中的那么简单。
1.2.3.2 BeanWrapper——对象的外衣
Spring
中的Bean
并不是以一个个的本来模样存在的,由于Spring IOC
容器中要管理多种类型的对象,因此为了统一对不同类型对象的访问,Spring
给所有创建的Bean
实例穿上了一层外套,这个外套就是BeanWrapper
BeanWrapper
实际上是对反射相关API
的简单封装,使得上层使用反射完成相关的业务逻辑大大的简化,我们要获取某个对象的属性,调用某个对象的方法,现在不需要在写繁杂的反射API
了以及处理一堆麻烦的异常,直接通过BeanWrapper
就可以完成相关操作
1.3 属性填充
上一步包裹在BeanWrapper
中的对象还是一个少不经事的孩子,需要为其设置属性以及依赖对象
-
基本类型属性
: 如果配置元信息中有配置,那么将直接使用配置元信息中的设置值赋值即可,即使基本类型的属性没有设置值,那么得益于JVM对象实例化过程,属性依然可以被赋予默认的初始化零值。 -
引用类型属性
:Spring
会将所有已经创建好的对象放入一个Map
结构中,此时Spring
会检查所依赖的对象是否已经被纳入容器的管理范围之内,也就是Map
中是否已经有对应对象的实例了。如果有,那么直接注入,如果没有,那么Spring
会暂时放下该对象的实例化过程,转而先去实例化依赖对象,再回过头来完成该对象的实例化过程。
在这里插入图片描述
我们可以看到第二步就是填充bean
的成员属性,populateBean
方法里面的逻辑大致就是对使用到了注入属性的注解就会进行注入,如果在注入的过程发现注入的对象还没生成,则会跑去生产要注入的对象,第三步就是调用initializeBean
方法初始化bean
,也就是调用我们上述所提到的接口
1.4 初始化bean
在这里插入图片描述可以看到initializeBean
方法中,首先调用的是使用的Aware
接口的方法,我们具体看一下invokeAwareMethods
方法中会调用Aware
接口的那些方法
1.4.1 Aware相关接口
在这里插入图片描述我们可以知道如果我们实现了BeanNameAware
,BeanClassLoaderAware
,BeanFactoryAware
三个Aware
接口的话,会依次调用setBeanName(), setBeanClassLoader(), setBeanFactory()
方法,再看applyBeanPostProcessorsBeforeInitialization
源码
1.4.2 BeanPostProcessors相关接口
在这里插入图片描述发现会如果有类实现了BeanPostProcessor
接口,就会执行postProcessBeforeInitialization
方法,这里需要注意的是:如果多个类实现BeanPostProcessor
接口,那么多个实现类都会执行postProcessBeforeInitialization
方法,可以看到是for
循环依次执行的,还有一个注意的点就是如果加载A类到spring
容器中,A类也重写了BeanPostProcessor
接口的postProcessBeforeInitialization
方法,这时要注意A类的postProcessBeforeInitialization
方法并不会得到执行,因为A类还未加载完成,还未完全放到spring
的singletonObjects
一级缓存中。
再看一个注意的点
在这里插入图片描述 图片可以看到ApplicationContextAwareProcessor
也实现了BeanPostProcessor
接口,重写了postProcessBeforeInitialization
方法,方法里面并调用了invokeAwareInterfaces
方法,而invokeAwareInterfaces
方法也写着如果实现了众多的Aware
接口,则会依次执行相应的方法,值得注意的是ApplicationContextAware
接口的setApplicationContext
方法,再看一下invokeInitMethods
源码
1.4.3 InitializingBean接口
在这里插入图片描述发现如果实现了InitializingBean
接口,重写了afterPropertiesSet
方法,则会调用afterPropertiesSet
方法,最后还会调用是否指定了init-method
,可以通过标签,或者@Bean
注解的initMethod
指定,最后再看一张applyBeanPostProcessorsAfterInitialization
源码图
1.4.4 BeanPostProcessors接口后置方法
在这里插入图片描述发现跟之前的postProcessBeforeInitialization
方法类似,也是循环遍历实现了BeanPostProcessor
的接口实现类,执行postProcessAfterInitialization
方法。整个bean
的生命执行流程就如上面截图所示,哪个接口的方法在哪里被调用,方法的执行流程。
1.5 bean生命周期总结
最后,对bean的生命流程进行一个流程图的总结
在这里插入图片描述
或者看简单版本:
在这里插入图片描述
2 三级缓存
2.1 引言
上面对bean
的生命周期做了一个整体的流程分析,对spring
如何去解决循环依赖的很有帮助。前面我们分析到填充属性时,如果发现属性还未在spring
中生成,则会跑去生成属性对象实例。
我们可以看到填充属性的时候,spring
会提前将已经实例化的bean
通过ObjectFactory
半成品暴露出去,为什么称为半成品是因为这时候的bean
对象实例化,但是未进行属性填充,是一个不完整的bean
实例对象
在实例化 Bean
之后,会往 singletonFactories
塞入一个工厂,而调用这个工厂的 getObject
方法,就能得到这个 Bean
spring
利用singletonObjects, earlySingletonObjects, singletonFactories
三级缓存去解决的,所说的缓存其实也就是三个Map
2.2 三级缓存各个存放对象
三级缓存各个存放对象:
-
一级缓存
,singletonObjects
,存储所有已创建完毕的单例 Bean (完整的 Bean) -
二级缓存
,earlySingletonObjects
,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean -
三级缓存
,singletonFactories
,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存
这三个 map 是如何获取配合的:
- 获取单例
Bean
的时候会通过BeanName
先去singletonObjects
(一级缓存) 查找完整的Bean
,如果找到则直接返回,否则进行步骤 2。 - 看对应的
Bean
是否在创建中,如果不在直接返回找不到,如果是,则会去earlySingletonObjects
(二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3 - 去
singletonFactories
(三级缓存)通过BeanName
查找到对应的工厂,如果存着工厂则通过工厂创建Bean
,并且放置到earlySingletonObjects
中。 -
如果三个缓存都没找到,则返回 null
在这里插入图片描述
可以看到三级缓存各自保存的对象,这里重点关注二级缓存earlySingletonObjects
和三级缓存singletonFactory
,一级缓存可以进行忽略。前面我们讲过先实例化的bean
会通过ObjectFactory
半成品提前暴露在三级缓存中
singletonFactory
是传入的一个匿名内部类,调用ObjectFactory.getObject()
最终会调用getEarlyBeanReference
方法。再来看看循环依赖中是怎么拿其它半成品的实例对象的。
2.3 解决循环依赖条件
在 Spring
中,只有同时满足以下两点才能解决循环依赖的问题:
- 必须是单例
依赖的Bean
必须都是单例
因为原型模式都需要创建新的对象,不能跟用以前的对象 - 不能全是构造器注入
依赖注入的方式,必须不全是构造器注入,且beanName
的字母顺序
在前的不能是构造器注入
在 Spring 中创建 Bean 分三步:
实例化
,createBeanInstance,就是 new 了个对象
属性注入
,populateBean, 就是 set 一些属性值
初始化
,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等
明确了上面这三点,再结合我上面说的“不完整的”,我们来理一下。
如果全是构造器注入,比如A(B b)
,那表明在new
的时候,就需要得到 B,此时需要new B
。但是 B 也是要在构造的时候注入 A ,即B(A a)
,这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到。
为什么找不到?因为 A 还没 new 完呢,所以找到不完整的 A,因此如果全是构造器注入的话,那么Spring
无法处理循环依赖 - 一个set注入,一个构造器注入能否成功
假设我们 A 是通过 set 注入 B,B 通过构造函数注入 A,此时是成功的
我们来分析下:实例化 A 之后,可以在 map 中存入 A,开始为 A 进行属性注入,发现需要 B,此时 new B,发现构造器需要 A,此时从 map 中得到 A ,B 构造完毕。
B 进行属性注入,初始化,然后 A 注入 B 完成属性注入,然后初始化 A。
整个过程很顺利,没毛病
假设 A 是通过构造器注入 B,B 通过 set 注入 A,此时是失败的
我们来分析下:实例化 A,发现构造函数需要 B, 此时去实例化 B。
然后进行 B 的属性注入,从 map 里面找不到 A,因为 A 还没 new 成功,所以 B 也卡住了,然后就 失败
看到这里,仔细思考的小伙伴可能会说,可以先实例化 B 啊,往 map 里面塞入不完整的 B,这样就能成功实例化 A 了啊
确实,思路没错但是Spring 容器是按照字母序创建 Bean 的,A 的创建永远排在 B 前面
现在我们总结一下:
- 如果循环依赖都是构造器注入,则失败
- 如果循环依赖不完全是构造器注入,则可能成功,可能失败,具体跟
BeanName
的字母序有关系
2.4 循环依赖示例说明
我们假设现在有这样的场景AService
依赖BService
,BService
依赖AService
-
AService
首先实例化,实例化通过ObjectFactory
半成品暴露在三级缓存中 - 填充属性
BService
,发现BService
还未进行过加载,就会先去加载BService
- 再加载
BService
的过程中,实例化,也通过ObjectFactory
半成品暴露在三级缓存 - 填充属性
AService
的时候,(从三级缓存通过对象⼯⼚拿到A,发现A虽然不太完善,但是存在,把A放⼊⼆级缓存,同时删除三级缓存中的A
,此时,B已经实例化并且初始化完成,把B放入⼀级缓存)这时候能够从三级缓存中拿到半成品的ObjectFactory
在这里插入图片描述
拿到ObjectFactory
对象后,调用ObjectFactory.getObject()
方法最终会调用getEarlyBeanReference()
方法,getEarlyBeanReference
这个方法主要逻辑大概描述下如果bean
被AOP
切面代理则返回的是beanProxy
对象,如果未被代理则返回的是原bean实例
。 - 接着A继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除⼆级缓存中的A,同时把A放⼊⼀级缓存
- 最后,⼀级缓存中保存着实例化、初始化都完成的A、B对象
2.5 是否可以移除二级缓存
我们发现这个二级缓存好像显得有点多余,好像可以去掉,只需要一级和三级缓存也可以做到解决循环依赖的问题
只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个bean
没被AOP
进行切面代理,如果这个bean
被AOP
进行了切面代理,那么只使用两个缓存是无法解决问题,下面来看一下bean
被AOP
进行了切面代理的场景
我们发现AService
的testAopProxy
被AOP
代理了,看看传入的匿名内部类的getEarlyBeanReference
返回的是什么对象。
发现singletonFactory.getObject()
返回的是一个AService
的代理对象,还是被CGLIB
代理的。再看一张再执行一遍singletonFactory.getObject()
返回的是否是同一个AService
的代理对象
我们会发现再执行一遍singleFactory.getObject()
方法又是一个新的代理对象,这就会有问题了,因为AService
是单例的,每次执行singleFactory.getObject()
方法又会产生新的代理对象。
假设这里只有一级和三级缓存的话,每次从三级缓存中拿到singleFactory
对象,执行getObject()
方法又会产生新的代理对象,这是不行的,因为AService
是单例的,所有这里我们要借助二级缓存来解决这个问题,将执行了singleFactory.getObject()
产生的对象放到二级缓存中去,后面去二级缓存中拿,没必要再执行一遍singletonFactory.getObject()
方法再产生一个新的代理对象,保证始终只有一个代理对象。还有一个注意的点
既然singleFactory.getObject()
返回的是代理对象,那么注入的也应该是代理对象,我们可以看到注入的确实是经过CGLIB
代理的AService
对象。所以如果没有AOP
的话确实可以两级缓存就可以解决循环依赖的问题,如果加上AOP
,两级缓存是无法解决的,不可能每次执行singleFactory.getObject()
方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象
转载于:
https://mp.weixin.qq.com/s/iCaBsXInpbNc8tiV-qRM4g
https://mp.weixin.qq.com/s/JVhRgiEcaNf6KLQehQwx1Q
网友评论