what?spring已经解决循环依赖了,为啥还报循环依赖错误?

前言

spring中的循环依赖三层map解决方案,八股文中重灾区,强如小伙伴们或许一字一句倒背如流了,当年的我也是如此,然而现实狠狠给了我一巴掌,报错虽迟但到

报错一:

The dependencies of some of the beans in the application context form a cycle

报错二:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘c’: Bean with name ‘c’ has been injected into other beans [d] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.

以上都是循环依赖导致,我纳闷了,不是说spring已经解决的循环依赖问题吗,为什么还会报错,说好的《java面试宝典》《java架构师指南》诚不欺我呢!

what?spring已经解决循环依赖了,为啥还报循环依赖错误?_第1张图片

一、什么是循环依赖?

在探索哪些场景会报错之前,让我们先来回顾一下,到底什么是循环依赖

spring 字段注入,相互持有对方引用,造成循环依赖的典型场景

@Service
 class A {

    @Resource
    private B b;
    
}

@Service
 class B {
 
    @Resource
    private A a;
    
}

有的小伙伴们开始想了,这种关系我直接用代码表示出来很简单呀,直接new出来,然后设置一下值就好了,为什么用spring的方式就出现循环依赖问题了。

java代码解决方案如下

A a = new A();
B b = new B();
//依次注入对方
a.setB(b);
b.setA(a);

对比发现,很容易得出以下结论:spring在ioc进程中,对象的生成需要流转复杂的bean生命周期,这才导致对象互相引用的时候产生了循环依赖问题。

问题又来了

那什么是bean的生命周期

bean的生命周期,由主线扩展点构成。

what?spring已经解决循环依赖了,为啥还报循环依赖错误?_第2张图片

本文章只需了解主线流程即可,其中主线为红色,扩展点为蓝色。每个bean对象的生成,都遵循以上过程。

那么,问题显而易见,bean A创建的时候走到空对象填充值节点时发现需要填充bean B,进而去创建bean Bbean B创建的时候走到空对象填充值节点时发现需要填充bean A,进而去创建bean A,好家伙,进入死循环了。这就是循坏依赖

二、如何解决循环依赖

回过头来想,我们自己写java代码的时候,很容易模拟并解决这个对象相互引用的场景呀,为什么spring做不到呢,究其根本,我们在创建对象的时候是可以拿到其他对象的引用,而spring对于没有创建完成的bean是不会加入到单例池中区的,意味着其他对象的引用不可见的。见节点spring缓存冲准备就绪的bean节点,最后才会加入到singletonObjects缓存中。

我们都知道彼此的存在,但我们都是瞎子。

所以,spring提供了三层map的机制,来解决bean中间状态的可见问题,核心逻辑如下。

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// 第一层缓存 创建好的bean
		Object singletonObject = this.singletonObjects.get(beanName);
		//isSingletonCurrentlyInCreation 代表获取的类已经开始初始化了,这里说明已经造成循环依赖了。
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				synchronized (this.singletonObjects) {
					// Consistent creation of early reference within full singleton lock
					singletonObject = this.singletonObjects.get(beanName);
					if (singletonObject == null) {
						//第二级缓存,解决中间状态bean对象的可见性问题。
						singletonObject = this.earlySingletonObjects.get(beanName);
						if (singletonObject == null) {
							//第三级缓存,解决动态代理的幂等性。
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
							if (singletonFactory != null) {
								singletonObject = singletonFactory.getObject();
								this.earlySingletonObjects.put(beanName, singletonObject);
								this.singletonFactories.remove(beanName);
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}

按访问顺序map排序

  • singletonObjects 存的是创建好的bean
  • earlySingletonObjects 半成品bean(实例化的空对象,还未填充值),由singletonFactories工厂生成。
  • singletonFactories提前进行aop代理,生成最终的代理类,保证引用不会变化。

spring的思路很简单,空对象填充值节点前,通过把半成品bean包装成工厂类放入singletonFactories提前暴露对象引用来保证可见性

//如果支持循环依赖的话就会进行提前暴露
		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");
			}
			//提前暴露,保证其他bean的可见性
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

		// Initialize the bean instance.
		Object exposedObject = bean;
		try {

			//空对象填充值
			populateBean(beanName, mbd, instanceWrapper);

			
			exposedObject = initializeBean(beanName, exposedObject, mbd);

		} catch (Throwable ex) {
			if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
				throw (BeanCreationException) ex;
			} else {
				throw new BeanCreationException(
						mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
			}
		}

提前暴露的能力靠第二层map就能实现了,为什么还需要搞个工厂来作为第三层map呢?

spring要保证,从map中拿到的引用是最终的引用,即对象的引用不会再改变了。我们想想哪些操作会导致对象引用会改变?大多数情况答案是:spring aop,spring aop的自动化编织,通过扩展点调用BeanPostProcessor的postProcessAfterInitialization()方法,定义BeanPostProcessor来实现,这将会导致对象引用的变化,工厂类的作用就是提前来进行aop操作,来保证获取到最终的对象

工厂类调用此方法生成第二层map对象

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;
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		return exposedObject;
	}


AbstractAutoProxyCreator aop自动编织的实现类,对该方法的重写,提前生成最终对象

	public Object getEarlyBeanReference(Object bean, String beanName) {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		this.earlyProxyReferences.put(cacheKey, bean);
		//编织切面
		return wrapIfNecessary(bean, beanName, cacheKey);
	}

从源码分析,可能存在不止AbstractAutoProxyCreator一种BeanPostProcessor会改变代理类的引用,spring aop 改变代理类只是大部分的情况,如果存在其他会改变代理类引用BeanPostProcessor一定要检查是否重写了getEarlyBeanReference方法,不然出现循环依赖的时候将会给你小惊喜

为什么我不叫它三级缓存呢?

我理解的层级缓存,应该是一级缓存没有,去二级缓存找,二级缓存没有去三级缓存找,类似cpu的L1L2L3缓存,每层缓存都可能会有同一个数据。而spring更像是三层map,因为bean对象实际上只会在其中一个map中。

三、哪些情况下spring循环依赖解决方案会失效?

纵观spring循坏依赖解决方案,还是有几个小问题:

无法解决构造函数注入,对应报错一

@Service
 class A {

    private B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }
}

@Service
 class B {

    private A a;


    @Autowired
    public B(A a) {
        this.a = a;
    }
}

spring循环依赖解决的就是就是中间状态对象的可见性问题,earlySingletonObjects层会缓存中间状态对象引用,如果beanA需要在构造函数结束前去创建beanB,这样无法产生中间状态的beanA,无法满足可见性问题,从而报错一;

解决方式如下

@Service
 class A {

    private B b;

    @Autowired
    public A(@Lazy B b) {
        this.b = b;
    }
}


通过添加@Lazy注解解决,具体在创建beanA时候引入beanB,不会真正去创建beanB,而是得到一个由@Lazy生成的代理类,beanA拿到该代理类创建完毕放入singletonObjects,beanB在真正使用的时候开始创建,这个时候beanA已经在singletonObjects中了,自然不会发生循坏依赖问题·。

如果存在不止一种BeanPostProcessor改变代理类的引用,没有重写getEarlyBeanReference方法,对应报错二

@Service
@Async
@EnableAsync
class C {

    @Resource
    private D d ;
}

@Service
public class D {

    @Resource
    private C c;
}

@Async会通过代理,通过BeanPostProcessor扩展点增强类的功能,提供类方法异步的能力,新生成的代理类会改变原始类对象的引用。我们来看看具体的实现类AsyncAnnotationBeanPostProcessor
what?spring已经解决循环依赖了,为啥还报循环依赖错误?_第3张图片
好家伙,完全没有重写getEarlyBeanReference,结果报错二,出处如下。

//earlySingletonReference 循环引用生成的代理类
		//exposedObject 本方法生成的代理类
		//bean  原始引用
		if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);
			//如果走到这里,说明触发了循环依赖
			if (earlySingletonReference != null) {
				//如果相等,这里是指发生了aop根据cacheKey判定到幂等
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
			//走到这里说明有其他钩子(后置处理器)把bean替换了,所以要检查在此之前是否已经发生过该bean的依赖注入,如果发生,就导致一个bean的不同版本被注入,针对这种情况,会抛出异常
				} else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
					for (String dependentBean : dependentBeans) {
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
							actualDependentBeans.add(dependentBean);
						}
					}
					if (!actualDependentBeans.isEmpty()) {
						throw new BeanCurrentlyInCreationException(beanName,
								"Bean with name '" + beanName + "' has been injected into other beans [" +
										StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
										"] in its raw version as part of a circular reference, but has eventually been " +
										"wrapped. This means that said other beans do not use the final version of the " +
										"bean. This is often the result of over-eager type matching - consider using " +
										"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
					}
				}
			}
		}

doCreateBean核心方法处以前看不懂的代码也可以解释了,代理对象的引用发生变化,导致spring认为有两个不同版本的bean加入到了容器内,对于这种情况spring也无能为力,只得抛出一个错,告诉程序员,有人在害你。

what?spring已经解决循环依赖了,为啥还报循环依赖错误?_第4张图片

四、总结

  • spring的三层map解决的根本问题就是中间状态对象的可见性
  • spring的三层map解决方案,仅适用于字段注入下的循环依赖场景
  • BeanPostProcessor改变对象引用的情况下,需要查看是否会发生循环引用,如果是,需要查看是否重写了getEarlyBeanReference方法,否则会报错。

你可能感兴趣的:(spring,java,面试)