Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >一张图彻底搞懂Spring循环依赖

一张图彻底搞懂Spring循环依赖

原创
作者头像
Tom弹架构
修改于 2021-10-29 07:43:42
修改于 2021-10-29 07:43:42
7630
举报
文章被收录于专栏:Tom弹架构Tom弹架构

1 什么是循环依赖?

如下图所示:

image.png
image.png
file
file

BeanA类依赖了BeanB类,同时BeanB类又依赖了BeanA类。这种依赖关系形成了一个闭环,我们把这种依赖关系就称之为循环依赖。同理,再如下图的情况:

image.png
image.png
file
file

上图中,BeanA类依赖了BeanB类,BeanB类依赖了BeanC类,BeanC类依赖了BeanA类,如此,也形成了一个依赖闭环。再比如:

image.png
image.png

上图中,自己引用了自己,自己和自己形成了依赖关系。同样也是一个依赖闭环。那么,如果出现此类循环依赖的情况,会出现什么问题呢?

2 循环依赖问题复现

2.1 定义依赖关系

我们继续扩展前面的内容,给ModifyService增加一个属性,代码如下:

代码语言:txt
AI代码解释
复制
@GPService
public class ModifyService implements IModifyService {

	@GPAutowired private QueryService queryService;
    
    ...

}

给QueryService增加一个属性,代码如下:

代码语言:txt
AI代码解释
复制
@GPService
@Slf4j
public class QueryService implements IQueryService {

	@GPAutowired private ModifyService modifyService;

    ...
    
}

如此,ModifyService依赖了QueryService,同时QueryService也依赖了ModifyService,形成了依赖闭环。那么这种情况下会出现什么问题呢?

2.2 问题复现

我们来运行调试一下之前的代码,在GPApplicationContext初始化后打上断点,我们来跟踪一下IoC容器里面的情况,如下图:

image.png
image.png
file
file

启动项目,我们发现只要是有循环依赖关系的属性并没有自动赋值,而没有循环依赖关系的属性均有自动赋值,如下图所示:

image.png
image.png
file
file

这种情况是怎么造成的呢?我们分析原因之后发现,因为,IoC容器对Bean的初始化是根据BeanDefinition循环迭代,有一定的顺序。这样,在执行依赖注入时,需要自动赋值的属性对应的对象有可能还没初始化,没有初始化也就没有对应的实例可以注入。于是,就出现我们看到的情况。

3 使用缓存解决循环依赖问题

image.png
image.png

3.1 定义缓存

具体代码如下:

代码语言:txt
AI代码解释
复制
// 循环依赖的标识---当前正在创建的实例bean
    private Set<String> singletonsCurrectlyInCreation = new HashSet<String>();

    //一级缓存
    private Map<String, Object> singletonObjects = new HashMap<String, Object>();

    // 二级缓存: 为了将成熟的bean和纯净的bean分离. 避免读取到不完整的bean.
private Map<String, Object> earlySingletonObjects = new HashMap<String, Object>();

3.2 判断循环依赖

增加getSingleton()方法:

代码语言:txt
AI代码解释
复制
/**
     * 判断是否是循环引用的出口.
     * @param beanName
     * @return
     */
    private Object getSingleton(String beanName,GPBeanDefinition beanDefinition) {

        //先去一级缓存里拿,
        Object bean = singletonObjects.get(beanName);
        // 一级缓存中没有, 但是正在创建的bean标识中有, 说明是循环依赖
        if (bean == null && singletonsCurrentlyInCreation.contains(beanName)) {

            bean = earlySingletonObjects.get(beanName);
            // 如果二级缓存中没有, 就从三级缓存中拿
            if (bean == null) {
                // 从三级缓存中取
                Object object = instantiateBean(beanName,beanDefinition);

                // 然后将其放入到二级缓存中. 因为如果有多次依赖, 就去二级缓存中判断. 已经有了就不在再次创建了
                earlySingletonObjects.put(beanName, object);


            }
        }
        return bean;
    }
		

3.3 添加缓存

修改getBean()方法,在getBean()方法中添加如下代码:

代码语言:txt
AI代码解释
复制
		 //Bean的实例化,DI是从而这个方法开始的
    public Object getBean(String beanName){

        //1、先拿到BeanDefinition配置信息
        GPBeanDefinition beanDefinition = regitry.beanDefinitionMap.get(beanName);

        // 增加一个出口. 判断实体类是否已经被加载过了
        Object singleton = getSingleton(beanName,beanDefinition);
        if (singleton != null) { return singleton; }

        // 标记bean正在创建
        if (!singletonsCurrentlyInCreation.contains(beanName)) {
            singletonsCurrentlyInCreation.add(beanName);
        }

        //2、反射实例化newInstance();
        Object instance = instantiateBean(beanName,beanDefinition);

        //放入一级缓存
        this.singletonObjects.put(beanName, instance);

        //3、封装成一个叫做BeanWrapper
        GPBeanWrapper beanWrapper = new GPBeanWrapper(instance);
        //4、执行依赖注入
        populateBean(beanName,beanDefinition,beanWrapper);
        //5、保存到IoC容器
        factoryBeanInstanceCache.put(beanName,beanWrapper);

        return beanWrapper.getWrapperInstance();
    
		}

3.4 添加依赖注入

修改populateBean()方法,代码如下:

代码语言:txt
AI代码解释
复制
    private void populateBean(String beanName, GPBeanDefinition beanDefinition, GPBeanWrapper beanWrapper) {

        ...

            try {

                //ioc.get(beanName) 相当于通过接口的全名拿到接口的实现的实例
                field.set(instance,getBean(autowiredBeanName));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                continue;
            }
        ...

    }

4 循环依赖对AOP创建代理对象的影响

4.1 循环依赖下的代理对象创建过程

我们都知道Spring AOP、事务等都是通过代理对象来实现的,而事务的代理对象是由自动代理创建器来自动完成的。也就是说Spring最终给我们放进容器里面的是一个代理对象,而非原始对象。

这里我们结合循环依赖,再分析一下AOP代理对象的创建过程和最终放进容器内的动作,看如下代码:

代码语言:txt
AI代码解释
复制
@Service
public class MyServiceImpl implements MyService {
    @Autowired
    private MyService myService;
    
    @Transactional
    @Override
    public Object hello(Integer id) {
        return "service hello";
    }
}

此Service类使用到了事务,所以最终会生成一个JDK动态代理对象Proxy。刚好它又存在自己引用自己的循环依赖的情况。跟进到Spring创建Bean的源码部分,来看doCreateBean()方法:

代码语言:txt
AI代码解释
复制
protected Object doCreateBean( ... ){
	
		...

		// 如果允许循环依赖,此处会添加一个ObjectFactory到三级缓存里面,以备创建对象并且提前暴露引用
		// 此处Tips:getEarlyBeanReference是后置处理器SmartInstantiationAwareBeanPostProcessor的一个方法,
		// 主要是保证自己被循环依赖的时候,即使被别的Bean @Autowire进去的也是代理对象
		// AOP自动代理创建器此方法里会创建的代理对象

		// Eagerly cache singletons to be able to resolve circular references
		// even when triggered by lifecycle interfaces like BeanFactoryAware.
		boolean earlySingletonExposure = (mbd.isSingleton() && 
													this.allowCircularReferences && 
													isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) { // 需要提前暴露(支持循环依赖),注册一个ObjectFactory到三级缓存
				addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

		// 如果发现自己被循环依赖,会执行上面的getEarlyBeanReference()方法,从而创建一个代理对象从三级缓存转移到二级缓存里
		// 注意此时候对象还在二级缓存里,并没有在一级缓存。并且此时可以知道exposedObject仍旧是原始对象	populateBean(beanName, mbd, instanceWrapper);
		exposedObject = initializeBean(beanName, exposedObject, mbd);
	
		// 经过这两大步后,exposedObject还是原始对象
		// 注意:此处是以事务的AOP为例
		// 因为事务的AOP自动代理创建器在getEarlyBeanReference()创建代理后,
	 // initializeBean() 就不会再重复创建了,二选一,下面会有详细描述)
	
		...
	
		// 循环依赖校验(非常重要)
		if (earlySingletonExposure) {
				// 前面讲到因为自己被循环依赖了,所以此时候代理对象还存放在二级缓存中
				// 因此,此处getSingleton(),就会把代理对象拿出来
				// 然后赋值给exposedObject对象并返回,最终被addSingleton()添加进一级缓存中
				// 这样就保证了我们容器里缓存的对象实际上是代理对象,而非原始对象

				Object earlySingletonReference = getSingleton(beanName, false);
				if (earlySingletonReference != null) {
	
						// 这个判断不可少(因为initializeBean()方法中给exposedObject对象重新赋过值,否则就是是两个不同的对象实例)
						if (exposedObject == bean) { 				
								exposedObject = earlySingletonReference;
						}
				}
				...
		}
	
}

以上代码分析的是代理对象有自己存在循环依赖的情况,Spring用三级缓存很巧妙的进行解决了这个问题。

4.2 非循环依赖下的代理对象创建过程

如果自己并不存在循环依赖的情况,Spring的处理过程就稍微不同,继续跟进源码:

代码语言:txt
AI代码解释
复制
protected Object doCreateBean( ... ) {
		...

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

		...
	
		// 此处注意,因为没有循环引用,所以上面getEarlyBeanReference()方法不会执行
		// 也就是说此时二级缓存里并不会存在
		populateBean(beanName, mbd, instanceWrapper);

		// 重点在此
		//AnnotationAwareAspectJAutoProxyCreator自动代理创建器此处的postProcessAfterInitialization()方法里,会给创建一个代理对象返回
		// 所以此部分执行完成后,exposedObject() 容器中缓存的已经是代理对象,不再是原始对象
	 // 此时二级缓存里依旧无它,更别提一级缓存了
	 exposedObject = initializeBean(beanName, exposedObject, mbd);

		...
	
		// 循环依赖校验
		if (earlySingletonExposure) {
				// 前面讲到一级、二级缓存里都没有缓存,然后这里传参数是false,表示不从三级缓存中取值
				// 因此,此时earlySingletonReference = null ,并直接返回

				// 然后执行addSingleton()方法,由此可知,容器里最终存在的也还是代理对象

				Object earlySingletonReference = getSingleton(beanName, false);
				if (earlySingletonReference != null) {
						if (exposedObject == bean) { 
								exposedObject = earlySingletonReference;
						}
				}
			 ...
}

根据以上代码分析可知,只要用到代理,没有被循环引用的,最终存在Spring容器里缓存的仍旧是代理对象。如果我们关闭Spring容器的循环依赖,也就是把allowCircularReferences设值为false,那么会不会出现问题呢?先关闭循环依赖开关。

代码语言:txt
AI代码解释
复制
// 它用于关闭循环引用(关闭后只要有循环引用现象将报错)
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
		public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        ((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(false);

    }
}

关闭循环依赖后,上面代码中存在A、B循环依赖的情况,运行程序会出现如下异常:

代码语言:txt
AI代码解释
复制
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)
	

此处异常类型也是BeanCurrentlyInCreationException异常,但报错位置在DefaultSingletonBeanRegistry.beforeSingletonCreation

我们来分析一下,在实例化A后给其属性赋值时,Spring会去实例化B。B实例化完成后会继续给B属性赋值,由于我们关闭了循环依赖,所以不存在提前暴露引用。因此B无法直接拿到A的引用地址,只能又去创建A的实例。而此时我们知道A其实已经正在创建中了,不能再创建了。所有就出现了异常。对照演示代码,来分析一下程序运行过程:

代码语言:txt
AI代码解释
复制
@Service
public class MyServiceImpl implements MyService {

	// 因为关闭了循环依赖,所以此处不能再依赖自己
	// 但是MyService需要创建AOP代理对象
    //@Autowired
    //private MyService myService;
    
    @Transactional
    @Override
    public Object hello(Integer id) {
        return "service hello";
    }
}

其大致运行步骤如下:

代码语言:txt
AI代码解释
复制
protected Object doCreateBean( ... ) {

		// earlySingletonExposure = false  也就是Bean都不会提前暴露引用,因此不能被循环依赖

		boolean earlySingletonExposure = (mbd.isSingleton() && 
													this.allowCircularReferences && 
													isSingletonCurrentlyInCreation(beanName));
		...

		populateBean(beanName, mbd, instanceWrapper);

		// 若是开启事务,此处会为原生Bean创建代理对象
		exposedObject = initializeBean(beanName, exposedObject, mbd);

		if (earlySingletonExposure) {
				... 

				// 因为上面没有提前暴露代理对象,所以上面的代理对象exposedObject直接返回。

		}
}

由上面代码可知,即使关闭循环依赖开关,最终缓存到容器中的对象仍旧是代理对象,显然@Autowired给属性赋值的也一定是代理对象。

最后,以AbstractAutoProxyCreator为例看看自动代理创建器实现循环依赖代理对象的细节。

AbstractAutoProxyCreator是抽象类,它的三大实现子类InfrastructureAdvisorAutoProxyCreator、AspectJAwareAdvisorAutoProxyCreator、AnnotationAwareAspectJAutoProxyCreator小伙伴们应该比较熟悉,该抽象类实现了创建代理的动作:

代码语言:txt
AI代码解释
复制
// 该类实现了SmartInstantiationAwareBeanPostProcessor接口 ,通过getEarlyBeanReference()方法解决循环引用问题

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

	...

	// 下面两个方法是自动代理创建器创建代理对象的唯二的两个节点:

	// 提前暴露代理对象的引用,在postProcessAfterInitialization之前执行
	// 创建好后放进缓存earlyProxyReferences中,注意此处value是原始Bean

	@Override
	public Object getEarlyBeanReference(Object bean, String beanName) {

			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			this.earlyProxyReferences.put(cacheKey, bean);
			return wrapIfNecessary(bean, beanName, cacheKey);

	}

	// 因为它会在getEarlyBeanReference之后执行,这个方法最重要的是下面的逻辑判断
	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {

		if (bean != null) {

			Object cacheKey = getCacheKey(bean.getClass(), beanName);

			// 下面的remove()方法返回被移除的value,也就是原始Bean
			// 判断如果存在循环引用,也就是执行了上面的getEarlyBeanReference()方法,
		    // 此时remove() 返回值肯定是原始对象
			
		    // 若没有被循环引用,getEarlyBeanReference()不执行
			// 所以remove() 方法返回null,此时进入if执行逻辑,调用创建代理对象方法
			if (this.earlyProxyReferences.remove(cacheKey) != bean) {
					return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}

		return bean;

	}
	...
}

根据以上分析可得知,自动代理创建器它保证了代理对象只会被创建一次,而且支持循环依赖的自动注入的依旧是代理对象。由上面分析得出结论,在Spring容器中,不论是否存在循环依赖的情况,甚至关闭Spring容器的循环依赖功能,它对Spring AOP代理的创建流程有影响,但对结果是无影响的。也就是说Spring很好地屏蔽了容器中对象的创建细节,让使用者完全无感知。

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【彻底搞懂】Spring之三级缓存解决循环依赖问题
spring解决循环依赖是通过对象的实例化和初始化分开的步骤来实现的,如果是构造函数注入的话,对象实例化就卡住了
西柚dzh
2022/06/09
3.8K2
【彻底搞懂】Spring之三级缓存解决循环依赖问题
Spring的循环依赖和三级缓存
(2)如果您不了解Spring Bean的声明周期,那么您可以看一下文章(Bean的生命周期_CBeann的博客-CSDN博客)或者百度其它文章,然后在回来看该文章,否则个人感觉应该看不懂
CBeann
2023/12/25
2200
Spring的循环依赖和三级缓存
烂了大街的 Spring 循环依赖问题,你觉得自己会了吗
初学 Spring 的时候,我们就知道 IOC,控制反转么,它将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,不需要我们手动去各种 new XXX。
海星
2020/09/09
6840
Spring源码解析(五):循环依赖
在完成Bean的实例化后,属性注入之前Spring将Bean包装成一个工厂对象添加进了三级缓存中,对应源码如下:
冬天vs不冷
2025/01/21
1960
Spring源码解析(五):循环依赖
一篇文章带你读懂Spring如何解决循环依赖!
当多个Bean相互依赖时则构成了循环依赖,例如A,B两个Bean。其中A中存在属性B,B中存在属性A,当Spring在实例化A时发现A中存在属性B,就去实例化B,实例化B时又发现存在属性A,一直在循环注入依赖,导致循环依赖问题出现。
Java程序猿
2021/06/25
4100
spring解决循环依赖
spring 单例对象的实例化、初始化过程是在doCreateBean中(之前仅仅是注册好了BeanDefenition), 大概分为三步:
leobhao
2022/06/28
5160
Spring的循环依赖,到底是什么样的
前一段时间,阿粉的读者给阿粉留言,说在面试的时候,有个面试官就问她,Spring 的各种知识,Spring 的生命周期, Spring 的循环依赖是如何解决的。
Java极客技术
2022/12/04
2800
Spring的循环依赖,到底是什么样的
Spring系列三之Bean实例化流程
紧接着上文,Spring的Bean实例化发生在刷新IOC容器阶段的倒数第二步finishBeanFactoryInitialization(beanFactory),最终在该方法中调用DefaultListable.preInstantiateSingletons()方法实例化所有非懒加载的Bean实例,代码如下
用户9511949
2024/09/05
1550
spring源码分析之如何解决循环依赖
spring-ioc中循环依赖的问题,也算是高频的面试问题了,今天跟大家一起来总结一下spring-ioc中是如何解决循环依赖的,相信大家是可以从这篇文章中彻底理解spring容器如何帮我们解决循环依赖,为了更好的理解spring-ioc如何解决循环依赖,大家可以先简单的了解spring-ioc中bean实例化的整个时序图。
全栈程序员站长
2022/07/04
4310
spring源码分析之如何解决循环依赖
Spring源码解析(五)Spring 加载bean 依赖注入
至此,我们分析过后,可以简单的归纳一下Spring IOC容器的过程 一、Resource定位过程 这个Resource定位指的的是BeanDefinition的资源定位,他由ResourceLoader通过统一的Resource接口来完成,这个Resource对各种形式的BeanDefinition的使用都提供了统一接口,对于这些BeanDefinition的存在形式,相信大家都不会感到陌生,比如,在类路径中的Bean定义信息可以使用FileSystemResouce来进行抽象;在类路径中的Bean定义信息可以使用前面提到的ClassPathResource来使用,等待;这个定位过程类似于容器寻找数据的过程,就像用水桶装水先要把水找到一样;
石臻臻的杂货铺[同名公众号]
2021/07/14
5830
堂妹让我聊:Spring循环依赖
在跟学弟学妹们聊完Spring IOC之后,有学弟反馈他们面试经常会遇到面试官询问Spring 里面的循环依赖问题。
敖丙
2021/09/16
3920
这次彻底搞懂IoC容器依赖注入的源码
初始化的过程,主要完成的工作是在容器中建立 BeanDefinition 数据映射,并没有看到容器对Bean依赖关系进行注入。
JavaEdge
2021/10/18
3940
Spring IOC原理补充(循环依赖、Bean作用域等)
通过之前的几篇文章将Spring基于XML配置的IOC原理分析完成,但其中还有一些比较重要的细节没有分析总结,比如循环依赖的解决、作用域的实现原理、BeanPostProcessor的执行时机以及SpringBoot零配置实现原理(@ComponentScan、@Import、@ImportSource、@Bean注解的使用和解析)等等。下面就先来看看循环依赖是怎么解决的,在此之前一定要熟悉整个Bean的实例化过程,本篇只会贴出关键性代码。
夜勿语
2020/09/07
5980
3.2spring源码系列----循环依赖源码分析
首先,我们在3.1 spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖 中手写了循环依赖的实现. 这个实现就是模拟的spring的循环依赖. 目的是为了更容易理解spring源码.
用户7798898
2020/11/11
5710
3.2spring源码系列----循环依赖源码分析
万字长文,助你深度遨游Spring循环依赖源码实现!
最近有很多读者面试的时候都被问到了有关于Spring三级缓存的解决方案,很多读者在面试受挫之后,试着自己去读源码,试着去跟断点又发现一层套一层,一会自己就懵了,我这几天总结了一下,为了能够让读者更加的去了解Spring解决循环依赖问题,我决定从以下四个方面去讲述:
止术
2020/09/15
5190
万字长文,助你深度遨游Spring循环依赖源码实现!
聊一聊Spring Bean 的生命周期
如果没有研究过 Spring 源码,单纯去背面试题,这个问题也是可以回答出来的,但是单纯的背缺乏理解,而且面试一紧张,就容易背岔了。但是如果你从头到尾看了松哥的 Spring 源码分析,那么这个问题就不需要背了,就根据自己对 Spring 源码的理解讲出来就行了。
江南一点雨
2024/01/03
2360
聊一聊Spring Bean 的生命周期
Spring 是如何解决循环依赖的?
正要创建的 bean 记录在缓存中,Spring 容器架构一个正在创建的 bean 标识符放在一个 “当前创建 bean 池”中国, 因此如果在创建 Bean 过程中,如果发现已经在当前创建的 Bean 池中,则抛出 BeanCurrentlyInCreationException 异常表示循环依赖,对于创建完毕的 Bean 将从“当前创建 Bean 池”中清除。 先看个例子:
王小明_HIT
2020/08/14
9000
Spring源码浅析——bean创建流程
本节我们看一下Spring如何解决循环依赖的问题。循环依赖的问题在很多语言场景上都会出现,比如说python中闭包导致的循环依赖:
用户1413827
2023/11/28
2430
Spring源码浅析——bean创建流程
Spring源码解析(四):单例bean的创建流程
冬天vs不冷
2025/01/21
1140
Spring源码解析(四):单例bean的创建流程
Spring如何解决循环依赖的
就是我们有两个服务,A服务,B服务,然后我们在A里注入了B,然后在B里注入了A,这就是循环依赖了,这种情况如果我们不解决的话,那就会出现一个相互依赖注入的死循环。
名字是乱打的
2021/12/24
1K0
Spring如何解决循环依赖的
相关推荐
【彻底搞懂】Spring之三级缓存解决循环依赖问题
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文