Spring面试题-Spring如何解决循环依赖?

文章目录

  • Spring面试题-Spring如何解决循环依赖?
    • 一、面试题
    • 二、验证(循环依赖的几种情况)
      • 2.1 构造循环依赖
      • 2.2 字段循环依赖
      • 2.3 Setter注入
      • 2.4 原型循环依赖
      • 2.5 混合循环依赖
    • 三、分析Spring如何解决循环依赖
      • 3.1 示意图
      • 3.2 如何解决属性注入循环依赖?
      • 3.3 为什么不能解决构造函数依赖注入
      • 3.4 为什么不能解决非单例依赖注入
      • 3.5 小结
    • 四、思考?
      • 4.1 如何维护缓存?
      • 4.2 缓存读写?
      • 4.3 为什么有三级缓存不是二级缓存?
      • 4.4 如果一个代理对象被循环依赖,如何保证最后返回到IOC的是代理对象?
      • 4.5 如果一个代理对象没有被循环依赖?
      • 4.6 关闭循环依赖?
    • 五、结论
    • 六、参考

Spring面试题-Spring如何解决循环依赖?

一、面试题

  • 问:Spring中循环依赖了解吗?

  • 答:是一个BeanA中依赖BeanB,然后BeanB中也依赖BeanA,当然也可以是更多Bean之间的循环依赖,A依赖B,B依赖C,C依赖A这种

  • 问:嗯,那你知道循环依赖有哪几种形式吗?

  • 答:嗯,因为我们依赖注入的时候,可以是属性注入,也可以是构造方法注入,也可以是setter方法注入,因此对应有三种循环依赖的形式。

  • 问:嗯,那你知道这几种循环依赖,哪些是没有问题的,哪些是程序会报错导致不能启动的吗?

  • 答:… 好像构造依赖会报错,其他两种是不会报错的,程序是可以正常启动的。

  • 问:嗯,那你知道为什么吗?为何构造循环依赖会报错?其他的循环依赖却可以正确的运行呢?

  • 答:这个…好像不太清楚啊,可以要看看Bean的生命周期才知道哦…

  • 问:嗯,那你可以简单说下Bean的生命周期吗?

答:
首先是AbstractApplicationContext的refresh方法刷新容器,然后初始化一些Bean的扩展接口,比如BeanDefinitionRegister这种,spring.factories的处理,
然后: 是InstantiationAwareBeanPostProcessor的实例化前置接口调用,他会在构造方法之前调用,构造之后还会有MergedBeanDefinitionPostProcessor处理,
然后: 就到属性填充阶段(populateBean),调用InstantiationAwareBeanPostProcessor后置接口,Autowire注入处理,
InstantiationAwareBeanPostProcessor的postProcessPropertyValues调用
然后: initializeBean进行Bean初始化,包括Aware系列接口调用,BeanPostProcessor前置方法,然后是相关的初始化方法,比如@Bean的init-method、
InitializingBean,后面BeanPostProcessor后置方法
然后:后面就是使用,销毁阶段...
  • 问:嗯,还算完整,那你结合这个Bean的生命周期,想一下你觉得循环依赖可以正确运行Spring是如何处理的呢?

  • 答:这个…没有研究过,我想可能会在属性注入的时候处理的吧…,还需要研究一下…

  • 现在我们来研究一下…

二、验证(循环依赖的几种情况)

  • 我们先看看几种循环依赖的情况

2.1 构造循环依赖

  • 这种情况,类A的构造方法中传入类B,同时类B的构造方法中传入类A,这种循环依赖会报错,Spring无法解决
@Component
public class Man {

    Person person;

    public Man(Person person) {
        this.person = person;
    }
}

@Component
public class Person {

    Man man;

    public Person(Man man) {
        this.man = man;
    }
}

报错如下:
Error creating bean with name 'man': Requested bean is currently in creation: Is there an unresolvable circular reference?
  • 错误提示我们可能有循环依赖。

2.2 字段循环依赖

  • 这种循环依赖,Spirng可以解决并且不会报错。
@Component
public class Man {

    @Autowired
    Person person;

}

@Component
public class Person {

    @Autowired
    Man man;
}

2.3 Setter注入

  • Setter注入和属性注入一致,可以解决;这里面我debug跟进去看,也是在A属性填充的过程(populateBean)的时候,会去获取B,然后B再去获取A,并从缓存中获取到A的早期引用,原理是一样的,先看后面。(这里是最后补充的,就直接写结论啦)
@Component
public class Man {
    Person person;

    @Autowired
    public void setPerson(Person person) {
        this.person = person;
    }
}

@Component
public class Person {
    Man man;

    @Autowired
    public void setMan(Man man) {
        this.man = man;
    }
}

2.4 原型循环依赖

  • 我们在第二步的基础之上,将两个类加上@Scope(“prototype”)注解,使用原型方式,测试报错如下,这里尤其注意一点,原型依赖的情况如何你不去getBean的时候是不会报错的,因为原型的Bean只有在使用的时候才会去创建,而且每次都是创建一个新的,因此使用下面的代码去获取就会报错了。
ApplicationContext app = new AnnotationConfigApplicationContext(Cap12MainConfig.class);
Person person = (Person) app.getBean("person");

报错如下:
Error creating bean with name 'man': Requested bean is currently in creation: Is there an unresolvable circular reference?

2.5 混合循环依赖

  • A构造依赖B,B属性依赖A;这种情况有可能不报异常,也有可能报异常(我测试有时候报错有时候不报错,估计和加载顺序有关系,没有深入研究了)

三、分析Spring如何解决循环依赖

3.1 示意图

  • 注意这个示意图只有主体流程,并不包含很详细的Bean的创建流程

Spring面试题-Spring如何解决循环依赖?_第1张图片

  • 后面的全部分析基于上面的示意图,假设我们AB相互循环依赖,上图中给出了创建A和B的过程中,整个流程图,左边的调用链是创建A,创建A的时候到了属性填充的步骤以为依赖B就会去创建B,然后创建B的时候,在B属性填充的时候需要去创建A,这是整个流程。

3.2 如何解决属性注入循环依赖?

  • 按照图中的灰色线我们先走一走,研究属性注入的循环依赖是如何解决的?
  • 首先看左列,创建A,先从缓存取->缓存没有(首次肯定没有)->getSingleton创建A,这里会把A放到一个正在创建集合表示A正在创建(code1),然后执行构造方法,构造执行完毕之后,会将自身放到缓存暴露早期引用(code2),然后再属性填充发现需要依赖B,因此去创建B。
//DefaultSingletonBeanRegistry
beforeSingletonCreation(beanName);

protected void beforeSingletonCreation(String beanName) {
		if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
		throw new BeanCurrentlyInCreationException(beanName);
	}
}

//code2
if (earlySingletonExposure) {
			if (logger.isDebugEnabled()) {
				logger.debug("Eagerly caching bean '" + beanName +
						"' to allow for resolving potential circular references");
			}
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}
  • 创建B的流程和A一模一样,也是走缓存,最后也会在属性填充的时候发现自己依赖A,然后就去创建A,等于又回到了创建A的逻辑,
  • 欣慰的是这一次A已经可以在缓存中获取了,得到了A(这个A其实还是不完整的),B就可以顺序初始化,B初始化好了,A也就可以初始化好了,由于对象在整个容器中是一个,最后A初始化完成之后,B所依赖的A也是一个完整的A了,完美解决。

3.3 为什么不能解决构造函数依赖注入

  • 按照图中的红色线我们先走一走,为什么不能解决构造函数依赖注入?
  • 首先创建A,缓存没有,将自己添加到正在创建的集合,然后构造的时候发现自己依赖B,因此转向获取B,关键在这里,此时A还未来得及将自己放到缓存
  • 然后获取B,也是一样,在构造的那一步发现自己依赖A,因此去获取A
  • 到这应该清楚了,缓存中获取不到A,因此需要走创建流程,而创建流程会将自己添加到正在创建的集合里面去,而A在之前已经添加过一次,这一次就会报错,如code1所示,下面的添加会返回false,因此会抛出BeanCurrentlyInCreationException异常
//singletonsCurrentlyInCreation是一个set集合,重复添加会返回false
!this.singletonsCurrentlyInCreation.add(beanName)

3.4 为什么不能解决非单例依赖注入

  • 非单例的每次创建都是new一个对象,IOC不会对其进行缓存,如果AB循环依赖,那么创建A1的时候依赖B1,然后要创建B1的时候发现依赖A,由此再去创建A2,然后A2有需要一个B2,一直下去没有尽头。不过Spring在代码中是有判断的,在AbstractBeanFactory的doGetBean中,如果缓存中获取Bean失败,就会判断需要获取的Bean是否为处于创建中的原型Bean,如果是就会抛出异常
// Fail if we're already creating this bean instance:
// We're assumably within a circular reference.
if (isPrototypeCurrentlyInCreation(beanName)) {
	throw new BeanCurrentlyInCreationException(beanName);
}
  • 这里就是检查原型Bena的循环依赖并且抛出异常的地方,在2.3中的堆栈日志也能够看到异常抛出的位置。

3.5 小结

  • 可以解决:属性循环依赖
  • 不能解决的情况一:单例bean,但依赖的成员变量在构造函数中初始化。
  • 不能解决的情况二:原型prototype作用域的bean发生循环依赖会直接抛出异常。
  • 不能解决的情况三:A构造依赖B,B属性依赖A,则有可能不报异常,也有可能报异常(我测试有时候报错有时候不报错,估计和顺序有关系)

四、思考?

4.1 如何维护缓存?

  • 缓存分级,一共有三级缓存
//DefaultSingletonBeanRegistry

/** Cache of singleton objects: bean name --> bean instance */ 
//一级缓存,缓存的是beanName -> 单例对象,保存的是完整的Bean(已创建完成的Bean)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of early singleton objects: bean name --> bean instance */ 
//二级缓存,缓存的是beanName -> 对象,保存的不完整的Bean(装配中的Bean)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);


/** Cache of singleton factories: bean name --> ObjectFactory */ 
//三级缓存,缓存的是beanName -> 创建Bean对象的工厂,(即将装配的Bean)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

4.2 缓存读写?

  • 从前面的流程图看到,构造方法执行完毕之后,对象会将自己放进去缓存,我们看看这个动作的细节:
//DefaultSingletonBeanRegistry
//1.这个步骤是构造方法完毕之后做的,将自己添加到缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));


//如果一级缓存没有的话,就会把自己放到三级缓存,并清空二级缓存
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(singletonFactory, "Singleton factory must not be null");
		synchronized (this.singletonObjects) {
		    //一级缓存没有才添加
			if (!this.singletonObjects.containsKey(beanName)) {
			    //添加三级缓存,清空二级缓存
				this.singletonFactories.put(beanName, singletonFactory);
				this.earlySingletonObjects.remove(beanName);
				this.registeredSingletons.add(beanName);
			}
		}
	}
	
//前面的lamada表达式看起来不直观,其实是实现了一个ObjectFactory,并保存到三级缓存里面,
//后续通过ObjectFactory执行getObject的时候就会调用getEarlyBeanReference这个方法
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;
	}
  • 我们再看看获取缓存的逻辑
//DefaultSingletonBeanRegistry
    @Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		//1.一级缓存获取,获取不到再去二级缓存获取
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
			    //2.二级缓存获取,获取不到再去三级缓存
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					//3.三级缓存获取,并移动到二级缓存,并清空三级缓存
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}
  • 我们看到,保存进去缓存的时候会存到三级缓存,并将二级缓存清除,获取的时候会依次寻找,如果在三级缓存获取,则移动到二级缓存,同时清空三级缓存,因此二级和三级缓存是互斥的。
  • 另外我们看到从三级缓存获取的时候会调用我们存进去的ObjectFactory的getObject用于获取bean,这里很有意思,其实是给我们预留了一个扩展接口,因为ObjectFactory的getObject其实就是走getEarlyBeanReference的逻辑,我们从前面的代码可以看到如果我们什么都不做,这里不会修改Bean,如果我们想修改Bean,程序在获取早期引用的时候能够做点什么小动作,我们就可以通过实现SmartInstantiationAwareBeanPostProcessor来做到,而这和AOP很有关系,后面会介绍。

4.3 为什么有三级缓存不是二级缓存?

  • 二级缓存可以解决这个问题吗?其实二级缓存是可以解决这个问题的,但是前面说了三级缓存的目的是将返回早起引用对象这个动作给用于预留一个扩展接口,在三级缓存中获取Bean调用的getEarlyBeanReference会回调SmartInstinationBeanPostProcessor这个接口的方法,开发者可以在这个阶段对返回的Bean对象做修改,如果AB循环依赖,B就会调用getEarlyBeanReference获取A,从而B会从三级缓存得到一个扩展后的A,并移动到二级缓存。
  • 换个角度假如只有2级缓存,那么用户扩展的动作就要放到二级缓存来做,这样每一次来获取的时候如果一级缓存没有都需要在二级缓存执行一次singletonFactory.getObject()触发一下获取Bean,这样这个方法很可能会被执行多次,每次都会创建一个新的对象,由此肯定会引起问题,引入了三级缓存singletonFactory.getObject()只会在第一次获取的时候,前两级缓存都不存在才会触发一次,并立刻将自己放到二级缓存,并清空三级缓存,后面每一次获取的都是这次创建的这个对象。
  • 如果不需要扩展,放进去的早期引用就是不完整的Bean,不考虑给其他后置处理器扩展,这样使用2级缓存是可以的。A直接将自己放入二级缓存,循环依赖的时候B直接从二级缓存获取A的早期引用保证B初始化成功再将自己加到一级缓存,然后A初始化成功后添加到一级缓存,但是这样就不能扩展了,因为这个扩展点很重要,在AOP的AnnotationAwareAspectJAutoProxyCreator就通过这个扩展点来保证代理对象的返回和代理对象的循环依赖问题解决(保证循环依赖的时候,返回的对象也是代理对象)
  • 综上来看三级缓存的目的是预留一共扩展接口,而这个接口和AOP有关。

4.4 如果一个代理对象被循环依赖,如何保证最后返回到IOC的是代理对象?

  • 如果A依赖B,B依赖A,A开启了代理,比如AOP或者事物,逻辑如下:
1.首先A初始化,然后在构造函数实例化之后,属性填充的时候,发现需要B,因此进入初始化B的流程;
2.初始化B,也和A前面一样,发现需要A就去获取A,获取A的时候,去三级缓存查询,第三级查到了,并且会调用getEarlyBeanReference,而getEarlyBeanReference会触发AOP中
核心类的父类AbstractAutoProxyCreator的getEarlyBeanReference执行,在这里会返回A的一个代理对象,由此B获得了A,B初始化成功
@Override
	public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		if (!this.earlyProxyReferences.contains(cacheKey)) {
			this.earlyProxyReferences.add(cacheKey);
		}
		return wrapIfNecessary(bean, beanName, cacheKey);
	}

3.回到A的初始化过程,A得到一个B对象,B进行属性填充,初始化,
  • 由此我们看到,即使A是一个代理对象,那么循环依赖的时候也能够保证依赖的是代理对象,这也能看到三级缓存的用处

4.5 如果一个代理对象没有被循环依赖?

  • 从前面4.2我们看到,一个对象A如果被B循环依赖,并且A是一个代理对象,那么Spring不仅解决了循环依赖并且可以保证依赖的也是代理对象,如果A是一个代理对象,但是没有被循环依赖,比如A就是一个纯粹被增强了的Bean会怎么样?
  • 如果A代理对象没有被循环依赖,那么他的getEarlyBeanReference就不会被调用(这个方法是在B循环依赖A并访问三级缓存的时候触发的),因此ProxyA会在AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization方法中,返回代理对象。因此在初始化完毕之后的循环依赖检查的时候,发现二级缓存中是没有的,因此会直接返回前面postProcessAfterInitialization创建的代理对象。
@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}
  • 这里注意了,前面4.2的getEarlyBeanReference方法和这里的postProcessAfterInitialization方法都是AOP核心方法,都在AbstractAutoProxyCreator里面实现,这两个方法就是创建增强的代理对象的方法,这里是和AOP相关的知识了,并且可以保证这两个方案只会被执行一次,这里好理解,创建代理后的增强对象也只要创建一次嘛,重复创建干啥,这里是通过earlyProxyReferences这个集合来实现的,如果前面创建过集合里面就会保存,后面的方法判断集合中有就不会重复创建了。

  • 这两个方法的调用时机,前置是循环依赖的时候,获取早起引用的时候调用,后者是正常的AOP逻辑在Bean的生命周期初始化后期BeanPostProcessor的后置方法中调用的。

  • 下面的检查是在初始化之后

//早期引用检查
if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);//false表示只查二级缓存
			//1.如果为null,说明二级缓存没有,说明本对象没有被循环依赖
			//(被循环依赖会的话将该Bean从三级缓存移动到二级缓存)
			//如果为null,没被循环依赖,就不走这个逻辑,直接走最后面返回,创建Bean完成
			//不为null,说明该对象被循环依赖了,那么此时需要检查exposedObject在初始化的过程中是否有被改变
			if (earlySingletonReference != null) {
			//如果没改,说明初始化过程没有修改,那么就把早期引用赋值给需要返回的对象
			//这里不知道为什么,难道是有可能早期引用暴露的是一个增强的代理,后面初始化就不会增强了
			//因此需要将之前的增强对象返回
			//另一方面,就是在前面的BeanPostProcessor里面,我们可以去在后置接口中registerSingleton往缓
			//存中设置一个自定义的对象,在这里earlySingletonReference就会获取到,由此替换掉原来的对象
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				//下面是依赖相关的处理
				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 " +
								"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
					}
				}
			}
		}

4.6 关闭循环依赖?

  • 关闭Spring的循环依赖能力,将allowCircularReferences设置为false,那么会出现什么,这里简单给出结论
1.关闭之后,创建A不会放入缓存,那么创建B再次创建A就会提示A正在创建中,因此会报错
2.关闭该选项,对于AOP返回代理没有影响,因为在初始化完成之后发现循环依赖被关闭,则直接返回代理对象,

这一块有兴趣可以自己看看源码

五、结论

  • Spring通过三级缓存来解决循环依赖
  • 为什么需要三级缓存,总而言之考虑到的是扩展性,预留扩展接口
  • 循环依赖和AOP结合的时候,也能保证依赖的是代理对象,是通过SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference接口回调来实现的

六、参考

  • [1] bean的循环依赖
  • [2] Spring IOC 容器源码分析 - 循环依赖的解决办法
  • [3] spring–解决循环依赖
  • [4] 一文告诉你Spring是如何利用"三级缓存"巧妙解决Bean的循环依赖问题的

你可能感兴趣的:(Spring)