Spring IOC系列学习笔记十三:循环依赖

原文地址


前言

  分析了ioc的创建流程,会遇到一个问题,如果出现循环依赖该如何解决,下面我们来具体的介绍一下之前学习的疑问

一、什么是循环依赖?

  所谓的循环依赖是指,A 依赖 B,B 又依赖 A,它们之间形成了循环依赖。或者是 A 依赖 B,B 依赖 C,C 又依 赖 A。它们之间的依赖关系如下:
在这里插入图片描述
循环依赖代码如下:

public class InstanceA  {
    @Autowired
    private InstanceB instanceB;  // InstanceA中依赖InstanceB
}

public class InstanceB  {
    @Autowired
    private InstanceA instanceA; // InstanceB中依赖InstanceA
}

二、解决循环依赖的思路

  为了更加理解循环依赖的解决思路,尝试通过手写伪代码代码来实现Bean的初始化过程,在这之前先分析一下一、二、三级缓存 存在的意义,以及解决了什么问题吧!
只使用一级缓存:
在这里插入图片描述
中间的闭环就是循环依赖。
  为了解决循环依赖导致的闭环问题,我们可以在闭环中增加一个出口,具体做法是:修改把bean放入一级缓存的时机,以前是属性赋值、初始化完成后才放进去,现修改为实例化完成后就先加入缓存中。并在获取某个对象时先去一级缓存中找一下,找到了直接返回,这样可以解决单线程的缓存依赖!如图所示
在这里插入图片描述
  但是这里又会带来一个新的问题,就是在多线程模式下,如果别的线程从一级缓存中获取到的是实例化后的类,这样明显是不可行的,因为这个类并不完整,是纯净的Bean,属性并没有真正被赋值!为了解决这个问题,二级缓存应运而生,初衷是分离完整bean和不完整的bean。
为什么要使用二级缓存?
  二级缓存作用是:暴露早期对象,为了将成熟bean 和 纯净bean 分离。
   还有一点关于bean的Aop动态代理的问题,我们都知道Bean的aop动态代理创建是在初始化之后,但是循环依赖的Bean如果使用了AOP。 那无法等到解决完循环依赖再创建动态代理, 因为这个时候已经注入属性。 所以如果循环依赖的Bean使用了aop. 需要提前创建aop。 此时使用二级缓存存储调动态代理的BeanPostProcessor也可以解决Aop的问题

if(二级缓存有说明是循环依赖?){  
     二级缓存 = 调用创建动态代理BeanPostProcessor返回代理bean(判断是否使用aop,没有依然返回原实例);
}

  所以说二级缓存确实完全可以解决循环依赖的任何情况:包括扩展能力(因为也可以在这里调用BeanPostProcessor, 当然AOP也是基于BeanPostProcessor,虽然也当然可以解决),那要三级缓存干啥?
为什么要是用三级缓存?
  其实三级缓存的说法众说纷纭,我们只能这样解释: Spring的方法职责都比较单例,一个方法通常只做一件事, getBean就是获取bean 但是调用创建动态代理BeanPostProcessor 是属于create的过程中的, 如果在这里明显代码比较耦合,阅读性也不太好。 所以为了解耦、方法职责单一、方便后期维护, 将调用创建动态代BeanPostProcessor 放在createBean中是最合适不过了, 但是我们判断当前是否循环依赖还是要写在getSingleton里面啊,这怎么办:
  三级缓存 存一个函数接口,函数接口实现 创建动态代理调用BeanPostProcessor 。为了避免重复创建, 调用把返回的动态代理对象或者原实例存储在二级缓存,三个缓存完美解决解耦、扩展、性能、代码阅读性。

三、使用了三级缓存还有什么问题?怎么解决的?

  使用了三级缓存,多线程模式下还是可能读取到不完整的bean,像这种线程安全问题,正常开发中基本遇不到,但是spring允许有这种操作,那spring怎么来解决的呢?答案肯定是加锁来解决。 spring中加了两把锁解决了线程安全问题! ,来看下Spring源码怎么解决的!
第一把锁

@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	
		 // 第一步:我们尝试去一级缓存获取
		Object singletonObject = this.singletonObjects.get(beanName);

		// 一级缓存没有,如标记为正在创建的话,就去二级缓存中找
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			//这里加锁,阻塞其他线程,等待属性赋值完成
			synchronized (this.singletonObjects) {
		
				//尝试去二级缓存中获取对象
				singletonObject = this.earlySingletonObjects.get(beanName);
				
				// 二级缓存中也没有获取到对象
				if (singletonObject == null && allowEarlyReference) {

					//直接从三级缓存中获取 ObjectFactory对象 这个对接就是用来解决循环依赖的关键所在
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					
					//从三级缓存中获取到对象不为空
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						
						//把早期对象放置在二级缓存,
						this.earlySingletonObjects.put(beanName, singletonObject);
						
						//ObjectFactory 包装对象从三级缓存中删除掉
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

  将要从二级缓存获取对象时加锁,此时阻塞其他线程,直到当前线程把对象属性赋值完成,并放入一级缓存中后,在允许其他线程执行!

	sharedInstance = getSingleton(beanName, () -> {
		try {
			//进入创建bean的逻辑
			return createBean(beanName, mbd, args);
		}

进入getSingleton创建中即可看到

	public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(beanName, "Bean name must not be null");
		//加锁
		synchronized (this.singletonObjects) {
			//其他线程从一级缓存获取一下成熟的bean
			Object singletonObject = this.singletonObjects.get(beanName);

		。。。。。 //省略代码

  这把锁锁的就是当前线程创建bean的过程,包括属性赋值、实例化、放入一级缓存都在锁范围内!保证了操作原子性。当前线程释放锁后,其他线程直接可以从一级缓存获取到成熟的bean!使用了两把锁就解决了线程安全问题!

四、二级缓存能否解决循环依赖,三级缓存存在的意义

我们先熟悉一下这三个缓存的意义:

名称描述
singletonObjects一级缓存,该缓存key = beanName, value = bean;这里的bean是已经创建完成的,该bean经历过实例化->属性填充->初始化以及各类的后置处理。因此,一旦需要获取bean时,我们第一时间就会寻找一级缓存
earlySingletonObjects二级缓存,该缓存key = beanName, value = bean;这里跟一级缓存的区别在于,该缓存所获取到的bean是提前曝光出来的,是还没创建完成的。也就是说获取到的bean只能确保已经进行了实例化,但是属性填充跟初始化肯定还没有做完,因此该bean还没创建完成,仅仅能作为指针提前曝光,被其他bean所引用
ingletonFactories三级缓存,不是用来存bean的实例,而是用来存函数接口、钩子函数的!该缓存key = beanName, value =beanFactory;在bean实例化完之后,属性填充以及初始化之前,如果允许提前曝光,spring会将实例化后的bean提前曝光,也就是把该bean转换成beanFactory并加入到三级缓存。在需要引用提前曝光对象时再通过singletonFactory.getObject()获取。

重新梳理循环依赖的流程如下:

  1. 实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行,A 只是一个半成品。
  2. 为 A 创建一个 Bean 工厂,并放入到 singletonFactories 中。
  3. 发现 A 需要注入 B 对象,但是一级、二级、三级缓存均为发现对象 B。
  4. 实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行,B 只是一个半成品。
  5. 为 B 创建一个 Bean 工厂,并放入到 singletonFactories 中。
  6. 发现 B 需要注入 A 对象,此时在一级、二级未发现对象 A,但是在三级缓存中发现了对象 A,从三级缓存中得到对象 A,并将对象 A放入二级缓存中,同时删除三级缓存中的对象 A。(注意,此时的 A 还是一个半成品,并没有完成属性填充和执行初始化方法)
  7. 将对象 A 注入到对象 B 中。
  8. 对象 B 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 B。(此时对象 B 已经是一个成品)
  9. 对象 A 得到对象 B,将对象 B 注入到对象 A 中。(对象 A 得到的是一个完整的对象 B)
  10. 对象 A 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 A。

  因此,Spring 一开始提前暴露的并不是实例化的 Bean,而是将 Bean 包装起来的 ObjectFactory。为什么要这么做呢

  这实际上涉及到 AOP,如果创建的 Bean 是有代理的,那么注入的就应该是代理 Bean,而不是原始的 Bean。但是 Spring 一开始并不知道 Bean 是否会有循环依赖,通常情况下(没有循环依赖的情况下),Spring 都会在完成填充属性,并且执行完初始化方法之后再为其创建代理。但是,如果出现了循环依赖的话,Spring 就不得不为其提前创建代理对象,否则注入的就是一个原始对象,而不是代理对象。因此,这里就涉及到应该在哪里提前创建代理对象?

  Spring 的做法就是在 ObjectFactory 中去提前创建代理对象。它会执行 getObject() 方法来获取到 Bean。实际上,它真正执行的方法如下:

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;
}

  通过上面的解析,我们可以知道 Spring 需要三级缓存的目的是为了在没有循环依赖的情况下,延迟代理对象的创建,使 Bean 的创建符合 Spring 的设计原则。

五、多例和构造器为什么无法解决循环依赖

为什么多例Bean不能解决循环依赖?

  只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以多例的bean是没有缓存的,不会将其放到三级缓存中。

为什么Spring不能解决构造器的循环依赖?

  循环依赖的解决是实例化后通过三级缓存来解决的,但构造器是在实例化时调用的,此时bean还没有实例化完成。如果此时出现了循环依赖,一二三级缓存并没有Bean实例的任何相关信息,因此当getBean的时候缓存并没有命中,这样就抛出了循环依赖的异常了。总结:spring的三级缓存解决的是实例化之后属性赋值的循环依赖,构造器被调用是在实例化之前,所以无法解决构造器的循环依赖!

六、扩展

上述已经介绍了为什么需要三级缓存其实就是spring的一个扩展的地方。在使用三级缓存解决缓存依赖的时候,把我们的早期对象包装成一个singletonFactory对象,该对象提供了一个getObject方法,该方法内部调用getEarlyBeanReference方法。

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		Object exposedObject = bean;
		//判读我们容器中是否有InstantiationAwareBeanPostProcessors类型的后置处理器
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			//获取我们所有的后置处理器
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				//判断我们的后置处理器是不是实现了SmartInstantiationAwareBeanPostProcessor接口
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					//进行强制转换
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					//挨个调用SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		//上面都不成立,返回原始bean
		return exposedObject;
	}

扩展案例

实现了SmartInstantiationAwareBeanPostProcessor 接口

@Component
public class TulingBPP implements SmartInstantiationAwareBeanPostProcessor {

   public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
      if(beanName.equals("instanceA") || beanName.equals("instanceB")) {
         JdkDynimcProxy jdkDynimcProxy = new JdkDynimcProxy(bean);
         return  jdkDynimcProxy.getProxy();
      }
      return bean;
   }
}

七、spring在创建bean的时候,在哪里创建的动态代理?

①:如果没有循环依赖的话,在bean初始化完成后创建动态代理
②:如果有循环依赖,在bean实例化之后创建!

八、spring是如何检测循环依赖的

循环依赖标识

/** 该集合缓存了当前正在创建bean的名称 */
	private final Set<String> singletonsCurrentlyInCreation =
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));

在创建一个bean时,会把这个bean放入循环依赖标识的集合中,在获取bean的时候,先从一级缓存中拿,如果一级缓存中没有,且循环依赖标识的集合中有,就说明这个类是发生了循环依赖

		/**
		 * 若在第一级缓存中没有获取到对象,并且singletonsCurrentlyInCreation这个list包含该beanName
		 * IOC容器初始化加载单实例bean的时候第一次进来的时候 该集合中一般返回空,但是循环依赖的时候可以满足该条件
		 */
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
		.....
		}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值