- 前言
- 循环依赖
- 构造器循环依赖
- property 范围的依赖处理
- setter 循环依赖
- 代码分析
- 解决场景
- 结合关键代码梳理流程
- 创建原始 bean
- addSingleFactory
- populateBean 填充属性
- getSingleton
- 总结
- 参考资料
循环依赖
循环依赖就是循环引用,就是两个或者多个 bean
相互之间的持有对方,最后形成一个环。例如 A
引用了 B
, B
引用了 C
, C
引用了 A
。
可以参照下图理解(图中展示的类的互相依赖,但循环调用指的是方法之间的环调用,下面代码例子会展示方法环调用):
如果学过数据库的同学,可以将循环依赖简单的理解为死锁,互相持有对方的资源,形成一个环,然后不释放资源,导致死锁发生。
在循环调用中,除非出现终结条件,否则将会无限循环,最后导致内存溢出错误。(我也遇到过一次 OOM,也是无限循环导致的)
书中的例子是用了三个类进行环调用,我为了简单理解和演示,使用了两个类进行环调用:
在 Spring
中,循环依赖分为以下三种情况:
构造器循环依赖
通过上图的配置方法,在初始化的时候就会抛出 BeanCurrentlyInCreationException
异常
publicstaticvoid main(String[] args){
// 报错原因: Requested bean is currently in creation: Is there an unresolvable circular reference?
ApplicationContext context =newClassPathXmlApplicationContext("circle/circle.xml");
}
从上一篇笔记中知道, Spring
容器将每一个正在创建的 bean
标识符放入一个 “当前创建 bean 池( prototypesCurrentlyInCreation
)” 中, bean
标识符在创建过程中将一直保持在这个池中。
检测循环依赖的方法:
分析上面的例子,在实例化 circleA
时,将自己 A
放入池中,由于依赖了 circleB
,于是去实例化 circleB
, B
也放入池中,由于依赖了 A
,接着想要实例化 A
,发现在创建 bean
过程中发现自己已经在 “当前创建 bean
” 里时,于是就会抛出 BeanCurrentlyInCreationException
异常。
如图中展示,这种通过构造器注入的循环依赖,是无法解决的。
property 范围的依赖处理
property
原型属于一种作用域,所以首先来了解一下作用域 scope
的概念:
在 Spring
容器中,在Spring容器中是指其创建的 Bean
对象相对于其他 Bean
对象的请求可见范围
我们最常用到的是单例 singleton
作用域的 bean
, Spring
容器中只会存在一个共享的 Bean
实例,所以我们每次获取同样 id
时,只会返回bean的同一实例。
使用单例的好处有两个:
- 提前实例化
bean
,将有问题的配置问题提前暴露 - 将
bean
实例放入单例缓存singletonFactories
中,当需要再次使用时,直接从缓存中取,加快了运行效率。
单一实例会被存储在单例缓存 singletonFactories
中,为Spring的缺省作用域.
看完了单例作用域,来看下 property
作用域的概念:在 Spring
调用原型 bean
时,每次返回的都是一个新对象,相当于 newObject()
。
因为 Spring
容器对原型作用域的 bean
是不进行缓存,因此无法提前暴露一个创建中的 bean
,所以也是无法解决这种情况的循环依赖。
setter 循环依赖
对于 setter
注入造成的依赖可以通过 Spring
容器提前暴露刚完成构造器注入但未完成其他步骤(如 setter
注入)的 bean
来完成,而且只能解决单例作用域的 bean
依赖。
在类的加载中,核心方法 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
,在这一步中有对循环依赖的校验和处理。
跟进去方法能够发现,如果 bean
是单例,并且允许循环依赖,那么可以通过提前暴露一个单例工厂方法,从而使其他 bean
能引用到,最终解决循环依赖的问题。
还是按照上面新建的两个类, CircleA
和 CircleB
,来讲下 setter
解决方法:
配置:
<!--注释 5.3 setter 方法注入-->
<beanid="circleA"class="base.circle.CircleA">
<propertyname="circleB"ref="circleB"/>
</bean>
<beanid="circleB"class="base.circle.CircleB">
<propertyname="circleA"ref="circleA"/>
</bean>
执行 Demo
和输出:
publicstaticvoid main(String[] args){
ApplicationContext context =newClassPathXmlApplicationContext("circle/circle.xml");
CircleA circleA =(CircleA) context.getBean("circleA");
circleA.a();
}
在 a 方法中,输出 A,在 b 方法中,输出B,下面是执行 demo 输出的结果:
错误提示是因为两个方法互相调用进行输出,然后打印到一定行数提示 main 函数栈溢出了=-=
A
B
A
B
*** java.lang.instrument ASSERTION FAILED ***:"!errorOutstanding" with message transform method call failed at JPLISAgent.c line:844
*** java.lang.instrument ASSERTION FAILED ***:"!errorOutstanding" with message transform method call failed at JPLISAgent.c line:844
*** java.lang.instrument ASSERTION FAILED ***:"!errorOutstanding" with message transform method call failed at JPLISAgent.c line:844
Exception in thread "main" java.lang.StackOverflowError
可以看到通过 setter
注入,成功解决了循环依赖的问题,那解决的具体代码是如何实现的呢,下面来分析一下:
代码分析
为了更好的理解循环依赖,首先来看下这三个变量(也叫缓存,可以全局调用的)的含义和用途:
/** Cache of singleton objects: bean name to bean instance. */
privatefinalMap<String,Object> singletonObjects =newConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
privatefinalMap<String,ObjectFactory<?>> singletonFactories =newHashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
privatefinalMap<String,Object> earlySingletonObjects =newHashMap<>(16);
变量 | 用途 |
singletonObjects | 用于保存 BeanName 和创建 bean 实例之间的关系, bean-name --> instanct |
singletonFactories | 用于保存 BeanName 和创建 bean 的 工厂 之间的关系, bean-name --> objectFactory |
earlySingletonObjects | 也是保存 beanName 和创建 bean 实例之间的关系,与 singletonObjects 的不同之处在于,当一个单例 bean 被放入到这里之后,那么其他 bean 在创建过程中,就能通过 getBean 方法获取到,目的是用来检测循环引用 |
之前讲过类加载的机制了,下面定位到创建 bean
时,解决循环依赖的地方:
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
// 是否需要提前曝光,用来解决循环依赖时使用
boolean earlySingletonExposure =(mbd.isSingleton()&&this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if(earlySingletonExposure){
if(logger.isTraceEnabled()){
logger.trace("Eagerly caching bean '"+ beanName +
"' to allow for resolving potential circular references");
}
// 注释 5.2 解决循环依赖 第二个参数是回调接口,实现的功能是将切面动态织入 bean
addSingletonFactory(beanName,()-> getEarlyBeanReference(beanName, mbd, bean));
}
protectedvoid addSingletonFactory(String beanName,ObjectFactory<?> singletonFactory){
Assert.notNull(singletonFactory,"Singleton factory must not be null");
synchronized(this.singletonObjects){
// 判断 singletonObjects 不存在 beanName
if(!this.singletonObjects.containsKey(beanName)){
// 注释 5.4 放入 beanName -> beanFactory,到时在 getSingleton() 获取单例时,可直接获取创建对应 bean 的工厂,解决循环依赖
this.singletonFactories.put(beanName, singletonFactory);
// 从提前曝光的缓存中移除,之前在 getSingleton() 放入的
this.earlySingletonObjects.remove(beanName);
// 往注册缓存中添加 beanName
this.registeredSingletons.add(beanName);
}
}
}
先来看 earlySingletonExposure
这个变量: 从字面意思理解就是需要提前曝光的单例。
有以下三个判断条件:
mbd
是否是单例- 该容器是否允许循环依赖
- 判断该
bean
是否在创建中。
如果这三个条件都满足的话,就会执行 addSingletonFactory
操作。要想着,写的代码都有用处,所以接下来看下这个操作解决的什么问题和在哪里使用到吧
解决场景
用一开始创建的 CircleA
和 CircleB
这两个循环引用的类作为例子:
A
类中含有属性 B
, B
类中含有属性 A
,这两个类在初始化的时候经历了以下的步骤:
- 创建
beanA
,先记录对应的beanName
然后将beanA
的创建工厂 beanFactoryA 放入缓存中 - 对
beanA
的属性填充方法populateBean
,检查到依赖beanB
,缓存中没有beanB
的实例或者单例缓存,于是要去实例化beanB
。 - 开始实例化
beanB
,经历创建beanA
的过程,到了属性填充方法,检查到依赖了beanA
。 - 调用
getBean(A)
方法,在这个函数中,不是真正去实例化beanA
,而是先去检测缓存中是否有已经创建好的对应的bean
,或者已经创建好的beanFactory
- 检测到
beanFactoryA
已经创建好了,而是直接调用ObjectFactory
去创建beanA