一张图彻底搞懂Spring循环依赖
wptr33 2025-05-11 18:52 5 浏览
1 什么是循环依赖?
如下图所示:
BeanA类依赖了BeanB类,同时BeanB类又依赖了BeanA类。这种依赖关系形成了一个闭环,我们把这种依赖关系就称之为循环依赖。同理,再如下图的情况:
上图中,BeanA类依赖了BeanB类,BeanB类依赖了BeanC类,BeanC类依赖了BeanA类,如此,也形成了一个依赖闭环。再比如:
上图中,自己引用了自己,自己和自己形成了依赖关系。同样也是一个依赖闭环。那么,如果出现此类循环依赖的情况,会出现什么问题呢?
2 循环依赖问题复现
2.1 定义依赖关系
我们继续扩展前面的内容,给ModifyService增加一个属性,代码如下:
@GPService
public class ModifyService implements IModifyService {
@GPAutowired private QueryService queryService;
...
}
给QueryService增加一个属性,代码如下:
@GPService
@Slf4j
public class QueryService implements IQueryService {
@GPAutowired private ModifyService modifyService;
...
}
如此,ModifyService依赖了QueryService,同时QueryService也依赖了ModifyService,形成了依赖闭环。那么这种情况下会出现什么问题呢?
2.2 问题复现
我们来运行调试一下之前的代码,在GPApplicationContext初始化后打上断点,我们来跟踪一下IoC容器里面的情况,如下图:
启动项目,我们发现只要是有循环依赖关系的属性并没有自动赋值,而没有循环依赖关系的属性均有自动赋值,如下图所示:
这种情况是怎么造成的呢?我们分析原因之后发现,因为,IoC容器对Bean的初始化是根据BeanDefinition循环迭代,有一定的顺序。这样,在执行依赖注入时,需要自动赋值的属性对应的对象有可能还没初始化,没有初始化也就没有对应的实例可以注入。于是,就出现我们看到的情况。
3 使用缓存解决循环依赖问题
3.1 定义缓存
具体代码如下:
// 循环依赖的标识---当前正在创建的实例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()方法:
/**
* 判断是否是循环引用的出口.
* @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()方法中添加如下代码:
//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()方法,代码如下:
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代理对象的创建过程和最终放进容器内的动作,看如下代码:
@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()方法:
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的处理过程就稍微不同,继续跟进源码:
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,那么会不会出现问题呢?先关闭循环依赖开关。
// 它用于关闭循环引用(关闭后只要有循环引用现象将报错)
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(false);
}
}
关闭循环依赖后,上面代码中存在A、B循环依赖的情况,运行程序会出现如下异常:
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其实已经正在创建中了,不能再创建了。所有就出现了异常。对照演示代码,来分析一下程序运行过程:
@Service
public class MyServiceImpl implements MyService {
// 因为关闭了循环依赖,所以此处不能再依赖自己
// 但是MyService需要创建AOP代理对象
//@Autowired
//private MyService myService;
@Transactional
@Override
public Object hello(Integer id) {
return "service hello";
}
}
其大致运行步骤如下:
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小伙伴们应该比较熟悉,该抽象类实现了创建代理的动作:
// 该类实现了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弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。私信回复"666"或关注微信公众号『 Tom弹架构 』可获取更多技术干货!
相关推荐
- Git 的设计:版本管理的天才之作
-
Git的设计之所以被称为“天才”,是因为它通过一系列**革命性的理念**和**精妙的工程实现**,完美解决了版本控制中的核心痛点。以下是其设计精髓的深度解析:---###一、**颠覆传统的核心设计...
- IDEA上如何重新设置git账号密码
-
修改git账号密码,使用IDEA拉取项目的文件,出现如下错误信息。fatal:unabletoaccess'http://192.168.105.101:8901/XX/XX-XX-p...
- 在IDEA中利用ignore插件忽略Git非必要提交的文件
-
迎关注我的头条号:Wooola,10年Java软件开发及架构设计经验,专注于Java、Go语言、微服务架构,致力于每天分享原创文章、快乐编码和开源技术。前言在IEDA中,使用Maven建多工程编写代码...
- Git入门系列之分支切换完全指南
-
在Git中切换分支时,若存在未提交的修改,需谨慎处理以避免数据丢失。主要注意事项包括:1)通过gitstatus检查当前修改;2)优先使用gitstash暂存修改再切换;3)可提交修...
- 解决Git推送提交时'src refspec master does not match any'错误
-
技术背景在使用Git进行代码管理时,我们经常会遇到各种问题。其中,“srcrefspecmasterdoesnotmatchany”错误是一个比较常见的问题。当我们尝试使用...
- 修改git提交的用户名
-
在我们项目中,新手同学们往往由于不当操作导致提交的用户名填写错误,当我们用户名填写错误的时候可以通过命令修改。获取当前用户名:gitconfiguser.name...
- 在项目管理中,自动关联Git代码提交,集成Gitlab/码云/Git等
-
不再让开发提交的代码成为“黑洞”在软件开发过程中,会有一个问题和担心,就是不知道开发人员提交的代码质量如何,规范怎样。更为让人痛苦的莫过于当时某个功能需求改了什么代码,根本无人知道。今天,分享一个工具...
- 常用的十五个Git命令汇总?
-
Git是一个分布式的版本管理系统,在开发中被广泛应用于代码版本的管理,下面就是在日常开发中常用的一些Git命令以及其示例操作,如下所示,我们就来一起看看吧!gitinit用于初始化一个新的Git仓库...
- git修改已提交记录的用户信息
-
背景介绍因为使用的是个人电脑,配置的git全局config的用户信息是和github的账户一致的。新下载的工作git,由于没有单独设置局部的用户信息,导致提交记录使用的是github用户,在push代...
- 刚来公司,大佬让我Git下 我该怎么办?
-
我们在开发过程中,都会用到版本控制工具。常用的工具有SVNGIt等。但现在越来越多的人喜欢用Git。本文为你介绍新手如何快速上手Git。创建自己的远程仓库远程仓库,顾名思义。就是将自己的代码放到远程服...
- Git+Maven+Sonar实现提交代码前进行代码的质量检查
-
Git+Maven+Sonar实现提交代码前进行代码的质量检查一、前言为了规范代码质量,使开发人员写出更高质量的代码,实践了一下git-hooks中的pre-commit钩子,可以在提交代码时强制校验...
- Git Rebase
-
本文档将深入讨论gitrebase命令。Rebase命令在设置仓库和重写历史页面中也有涉及。本页将更详细地介绍gitrebase的配置和执行。这里将涵盖常见的Rebase使用场景和注...
- Git提交规范
-
一、分区存储...
- git合并分支时禁止合并特定文件
-
问题:1.在日常开发过程中经常会遇到多环境,但是环境文件不同的情况,导致每次切换git环境时候非常麻烦,可能会提交上来不需要提交的文件,一个文件来回提交修改。场景:...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git 执行pull错误如何撤销 git pull fail
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git pull 之后本地代码被覆盖 解决方案
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)