灵魂画手图解 Spring 循环依赖
(给 java金融 加星标,提高Java技能)
前言
想彻底弄清楚 Spring 的循环依赖问题,首先得弄清楚,
-
循环依赖是如何发生的,Spring 又是如何检测循环依赖的发生的。
-
其次再探究 Spring 如何解决循环依赖的问题。
最后,我们将总结循环依赖解决的两个关键因素,提前曝光和曝光时机,缺一不可。
1、循环依赖检查
<bean id="a" class="A">
<property name="b" ref="b">
<bean/>
<bean id="b" class="B">
<property name="a" ref="a">
<bean/>
无论单例还是原型模式 (下文①代表图中步骤 1),Spring 都有对应的集合保存当前正在创建的 beanName,标识该 beanName 正在被创建。在 bean 创建前,①检测当前 bean 是否在创建中,如果不在创建中则②将 beanName 加入集合,往下创建 bean。在 bean 创建前,检测到当前的 bean 正在创建,则说明发生循环依赖,抛出异常。最后记得当 bean 创建完时将 beanName 移出集合。
2、循环依赖的处理
单例 setter 循环依赖
Spring 注入属性的方式有多种,但是只有一种循环依赖能被解决:setter 依赖注入。前面或多或少都提到了 Spring 解决循环依赖的做法是未等 bean 创建完就先将实例曝光出去,方便其他 bean 的引用。同时还提到了三级缓存,最先曝光到第三级缓存 singletonFactories 中。简单的说,就是 Spring 先将创建好的实例放到缓存中,让其他 bean 可以提前引用到该对象。
示例
// 第一种 注解方式
public class A {
@Autowired
private B b;
}
public class B {
@Autowired
private A a;
}
// ===========================
// 第二种 xml配置方式
public class A {
private B b;
// getter setter
}
public class B {
private A a;
// getter setter
}
<bean id="a" class="A">
<property name="b" ref="b">
<bean/>
<bean id="b" class="B">
<property name="a" ref="a">
<bean/>
分析
上图我觉得我画的很满意,堪称灵魂画手。其中跟循环依赖检测对比,新添加的几个关键节点已经用黄色标识出来,这里有几个重点给大家画一下。
-
提前曝光,如果用 C 语言的说法就是将指针曝光出去,用 java 就是将引用对象曝光出去。也就是说即便 a 对象还未创建完成,但是在④实例化过程中 new A() 动作已经开辟了一块内存空间,只需要将该地址抛出去 b 就可以引用的到,而不管 a 后期还会进行初始化等其他操作;
-
已经了解了提前曝光的作用,而相比而言⑤曝光的时机也非常的重要,该时机发生在④实例化之后,⑥填充与⑯ 初始化之前。Spring 循环依赖之所以不能解决实例化注入的原因正式因为注入时机在曝光之前所导致;
-
⑤中写的带 a 的工厂是什么东西?先来了解一下 ObjectFatory。
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}
就是一个接口,通过重写 getObject() 方法返回对应的 object。
// 将该bean提前曝光,具体做法是创建一个ObjectFactory对象,再将对象加入到singletonFactories缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
让我帮大家改写一下,不然可能看了有点懵逼,以上代码等同于:
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
getEarlyBeanReference(beanName, mbd, bean);
}
});
但是我们看到,按原计划重写 getObject() 应该是直接 return bean 就行了,为什么还有 getEarlyBeanReference 是什么鬼?(这点非常重要,但是我看了很多博客甚至书本都完全忽视了这点,如果忽视了这点,那三级缓存将失去意义,直接二级缓存就可以解决提前曝光的问题)
getEarlyBeanReference 目的就是为了后置处理,给一个在提前曝光时操作 bean 的机会,具体要怎么操作 bean,那就继承 SmartInstantiationAwareBeanPostProcessor 重写 getEarlyBeanReference 方法吧。比如你要 System。out。print (“啊啊啊啊,我是” + bean + “,我被曝光且提前引用啦”) 也是可以的,关键就在于 bean 被曝光到三级缓存时并没用使用提前曝光的后置处理,而是当三级缓存被提前引用到二级缓存时才触发!(但是在 Spring 的源码中,真正实现这个方法的只有 AbstractAutoProxyCreator 这个类,用于提前曝光的 AOP 代理。
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
// 这么一大段就这句话是核心,也就是当bean要进行提前曝光时,
// 给一个机会,通过重写后置处理器的getEarlyBeanReference方法,来自定义操作bean
// 值得注意的是,如果提前曝光了,但是没有被提前引用,则该后置处理器并不生效!!!
// 这也正式三级缓存存在的意义,否则二级缓存就可以解决循环依赖的问题
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
单例构造器注入循环依赖
上面已经剧透了这个方式是不行的,原因是依赖注入的时间点不对,他的依赖注入发生在构造器阶段,这个时候连实例都没有,内存都还没开辟完,当然也还没有进行提前曝光,因此不行。
示例
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
public class B {
private A a;
public B(A a) {
this.a = a
}
}
分析
图上重点地方也用黄色标出了,问题的原因处在④实例化,实例化的过程是调用 new A (B b); 的过程,这时的 A 还未创建出来,根本是不可能提前曝光的,正是这个原因导致⑨无法获取到三级缓存,进而导致⑩异常的抛出。
原型模式循环依赖
这此没有图了,因为原型模式每次都是重新生成一个全新的 bean,根本没有缓存一说。这将导致实例化 A 完,填充发现需要 B,实例化 B 完又发现需要 A,而每次的 A 又都要不一样,所以死循环的依赖下去。唯一的做法就是利用循环依赖检测,发现原型模式下存在循环依赖并抛出异常。
总结
总结一下循环依赖,Spring 只能解决 setter 注入单例模式下的循环依赖问题。要想解决循环依赖必须要满足两个条件:
- 需要用于提前曝光的缓存;
- 属性的注入时机必须发生在提前曝光动作之后,不管是填充还是初始化都行,总之不能在实例化,因为提前曝光动作在实例化之后。
理解了这两点就可以轻松驾驭循环依赖了。比如构造器注入是不满足第二个条件,曝光时间不对。而原型模式则是缺少了第一个条件,没有提前曝光的缓存供使用。
转自:bugpool,
链接:blog.csdn.net/chaitoudaren/article/details/104833575
- EOF -
看完本文有收获?请转发分享给更多人
关注「java金融」,提升Java技能
点赞和在看就是最大的支持 ❤️