Spring循环依赖产生与解决
码畜c 人气:0循环依赖产生情景
探讨如何解决循环依赖之前,更应该思考清楚什么情况下会发生这种问题?
1、模拟Prototype Bean的循环依赖
static class BeanA { // 1. 属性循环依赖 BeanB beanB = new BeanB(); // 2. 构造器循环依赖 public BeanA(BeanB beanB) { this.beanB = beanB; } } static class BeanB { // 1. 属性循环依赖 BeanA beanA = new BeanA(); // 2. 构造器循环依赖 public BeanB(BeanA beanA) { this.beanA = beanA; } } public static void main(String[] args) { // 1. 属性循环依赖 BeanA beanA = new BeanA(); // StackOverflowError // 2. 构造器循环依赖 new BeanA(new BeanB(new BeanA(new BeanB( /*....*/ )))); }
Prototype bean,构造器注入Bean时,强调注入的是成品Bean,无法解决循环依赖问题。
Prototype bean,属性上注入Bean时,由于原型模式是单个bean可以被多次创建的,一旦发生循环依赖,就会产生上面这种不断在创建新的BeanA与BeanB导致的栈溢出。源码中的解决方式:记录并检测,如下:
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { // ... // Fail if we're already creating this bean instance: // We're assumably within a circular reference. if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } // ... else if (mbd.isPrototype()) { // It's a prototype -> create a new instance. Object prototypeInstance = null; try { beforePrototypeCreation(beanName); prototypeInstance = createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } // ... } protected boolean isPrototypeCurrentlyInCreation(String beanName) { Object curVal = this.prototypesCurrentlyInCreation.get(); return (curVal != null && (curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName)))); } protected void beforePrototypeCreation(String beanName) { Object curVal = this.prototypesCurrentlyInCreation.get(); if (curVal == null) { this.prototypesCurrentlyInCreation.set(beanName); } else if (curVal instanceof String) { Set<String> beanNameSet = new HashSet<>(2); beanNameSet.add((String) curVal); beanNameSet.add(beanName); this.prototypesCurrentlyInCreation.set(beanNameSet); } else { Set<String> beanNameSet = (Set<String>) curVal; beanNameSet.add(beanName); } }
获取Bean前先判断是否当前Bean是否为Prototype并且在创建中。如果不是,且当前Bean为Prototype,那么会记录当前Bean正在创建中(通过beforePrototypeCreation方法)。
以模拟循环依赖代码为例:
- 创建BeanA
- populateBean(BeanA)
- 创建BeanB
- populateBean(BeanB)
- 创建BeanA,isPrototypeCurrentlyInCreation发现BeanA正常创建中,抛出BeanCurrentlyInCreationException
2、模拟Singleton Bean的循环依赖
static class BeanA { BeanB beanB; public BeanA(BeanB beanB) { this.beanB = beanB; } } static class BeanB { BeanA beanA; public BeanB(BeanA beanA) { this.beanA = beanA; } } public static void main(String[] args) { // 1. 属性循环依赖 BeanA beanA = new BeanA(); BeanB beanB = new BeanB(); beanA.beanB = beanB; beanB.beanA = beanA; // 2. 构造器循环依赖 new BeanA(new BeanB(new BeanA(new BeanB( /*....*/ )))); }
Singleton bean,构造器注入Bean时,强调注入的是成品Bean,无法解决循环依赖问题。
Singleton bean,属性上注入Bean时,允许注入提前暴露的半成品的Bean,即没有填充属性&初始化的Bean,只要引用关系能关联上,属性填充与初始化随着程序的执行自然就会处理完成,可以解决循环依赖。
为什么构造器中不能先传递一个半成品Bean,然后赋值给成员Bean属性呢?而一定要传递一个成品Bean?很好理解,spring并不能确定用户在构造器中的操作仅为赋值给Bean属性。且很多时候,我们需要通过bean的一些注入的Bean属性来执行一些业务操作的。如果为空,那么就可能会发生空指针异常。而且这样也不是特别合理。若用户选择属性注入的方式,用户不会涉及这些操作,那么自然就不需要考虑这些问题。
3、总结成一句话来说:spring仅能解决单例Bean下发生在属性上注入Bean产生的循环依赖。
Spring如何解决循环依赖
通过三级缓存解决循环依赖,分别是:
1、singletonObjects: 一级缓存,存储成品Bean
2、earlySingletonObjects: 二级缓存,存储半成品Bean
3、singletonFactories: 三级缓存,存储生成半成品Bean的ObjectFactory的lambda
还是以BeanA注入BeanB,BeanB注入BeanA为例,描述一下大致的执行流程:
- 检查BeanA的缓存信息
- 反射实例化BeanA
- 注册暴露 BeanA的lambda(生成放入二级缓存中的半成品BeanA)到三级缓存:addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))
- 填充属性BeanB:populateBean
- 检查BeanB的缓存信息
- 注册暴露 BeanB的lambda(生成放入二级缓存中的半成品BeanB)到三级缓存
- 反射实例化BeanB
- 填充属性BeanA:populateBean
- 检查BeanA的缓存信息,从三级缓存中获取到了生成半成品BeanA的lambda,执行获取半成品BeanA并放入二级缓存,并在三级缓存中移除lambda
- 将生成的BeanA的二级缓存对象赋值到BeanB的属性上
- 将BeanB放入一级缓存,并在二、三级缓存中清理关于BeanB的记录
- 初始化BeanB
- 返回到第四步,将成品BeanB赋值到BeanA的属性上
- 将BeanA放入一级缓存,并在二、三级缓存中清理关于BeanA的记录
- 初始化BeanA
思考一下,是否能通过二级缓存来解决循环依赖?
首先spring对于三级缓存的应用,就是在生成需要提前暴露的半成品Bean,要么返回的是通过反射实例化后的对象,要么是被一组SmartInstantiationAwareBeanPostProcessor处理后的对象(比如生成代理Bean),然后在放到二级缓存中。那么我们在放入二级缓存时,不通过三级缓存获取这个过程,直接在方法中复现这个过程,在放入二级缓存中,效果想必也是相同的。
加载全部内容