Spring循环依赖问题——从源码画流程图

文章目录

循环依赖 在线流程图

关键代码

从缓存中查询getSingleton()方法

java 复制代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock
    // 一级缓存 二级缓存中取
    Object singletonObject = this.singletonObjects.get(beanName);
    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) {
                    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;
}

经过单例Bean判断和是否允许开启依赖注入判断后,往第三级缓存中存ObjectFactory函数式接口对象

java 复制代码
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");
    }
    // 循环依赖-添加到三级缓存
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

初始化后将循环依赖产生的代理对象赋值给普通对象

java 复制代码
	if (earlySingletonExposure) {
        // 从二级缓存中取代理对象
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
                // 赋值给当前普通对象
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					// beanName被哪些bean依赖了,现在发现beanName所对应的bean对象发生了改变,那么则会报错
					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.");
					}
				}
			}
		}

相关知识

案例:A对象 与 B对象互相引用,A对象 与 C对象互相引用。

为什么要使用二级缓存

作用:

  • 二级缓存中存储的是不完整的早期的bean。
  • 二级缓存使不完整的bean和完整的bean分开存储,保证并发线程安全,提高性能

假如不使用二级缓存,只有一级缓存的情况。一级缓存它想要打破循环的话就只能把半成品Bean存放在一级缓存中,这样创建B对象时就获取到一级缓存中的半成品A对象了,这样也就打破了对象互相引用的死循环。

问题是多线程访问时,其他线程可能就拿到了A对象取使用了。

解决方法是对整个getBean()方法加锁,这样的问题是锁的粒度太大了,而且想获取已经创建好了的对象也需要等待

所以就加入了二级缓存,用二级缓存来降低锁的粒度,提升性能,

为什么要使用三级缓存

作用:

  • 解决循环依赖的死循环
  • 使用ObjectFactory函数式接口,提升bean创建过程扩展性,保证规范,代码职责单一性

如果不使用三级缓存,同时A对象要进行AOP的话,此时就会出现依赖注入给B对象中的属性的A的普通对象,A经过创建过程之后存入单例池的却是代理对象

如果想要解决上面的问题就需要在创建bean对象的过程中,在实例化之后,属性填充之前就进行AOP操作生成代理对象,并存入二级缓存中。

这种方式就破坏了Bean创建过程的规范,同时还会出现循环依赖多次创建多次动态代理对象。

所以解决方法是我只判断出现了循环依赖这种情况才对该bean提前进行AOP操作。这些操作代码我不能加在创建bean的各个小步骤中,为了保证代码职责单一性。所以便有了三级缓存来保存ObjectFactory函数式接口对象,在出现了循环依赖的情况下就去执行对应方法。

只使用两个缓存的问题

如果只使用一级和二级缓存,那么就需要考虑AOP问题,可能给对象B注入的是普通对象,存入单例池是的代理对象

如果只使用一级和三级缓存,A对象与B对象互相引用,A对象与C对象互相引用。在执行行A对象就会创建两个AOP代理对象分别注入给B和C

不能解决构造器循环依赖

因为在创建Bean的总流程中,先进行实例化,完成之后才会去往三级存储中存入数据。此时实例化都没有完成,普通对象都没有创建成功,所以无法处理

解决方法是使用@Lazy注解

为什么多例bean不能解决循环依赖问题

我们从源码中是可以发现,再往三级缓存中存数据前是进行了if判断的,只有单例bean才会加入到三级缓存中。

其实解决循环依赖核心就是使用了一个Map,而这个map就相当于一个缓存。

我们bean是单例的,而且注入是通过setter字段注入的,单例意味着只需要创建一次对象,后续就可以从缓存中取出来,字段注入意味着我们无需调用构造方法进行注入。

  • 如果是原型Bean,那么意味着每次都要去创建bean,无法利用缓存
  • 如果是构造方法注入,那么意味着需要调用构造方法注入,也无法利用缓存

初始化后代理对象赋值给原始对象

在bean的创建过程中,还有一个地方跟循环依赖有关。

如果A是动态代理对象,通过三级缓存循环依赖之后,就会出现B.a = A的代理对象。但进行了初始化整个过程的A对象是普通对象,所以此时就需要把二级缓存中的代理对象赋值给BeanA。

java 复制代码
	if (earlySingletonExposure) {
        	// 1. 从二级缓存中取出代理对象
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
                // 2. 赋值给普通对象
                // 这里会判断初始化过程中是否改变了当前对象,如果改变了则下面的if判断不成立
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					// beanName被哪些bean依赖了,现在发现beanName所对应的bean对象发生了改变,那么则会报错
					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.");
					}
				}
			}
		}
  • @Async注解会在初始化过程中创建动态代理对象,导致上面if判断中得到的代理对象和原始bean对象不一致。故而抛出异常
  • @Transactional注解也是代理对象,为什么不会报错?这是因为@Transaction注解的代理对象和AOP实际上是一个代理对象。

在解决循环依赖时,我就通过了普通对象创建了一个代理对象,解决你在初始化过程中有改变了普通对象,那我之前创建的代理对象岂不是没用了。

解决循环依赖

如果关闭了循环依赖功能,如果有两种方式解决循环依赖

  1. 使用@Lazy注解加在属性上,代表Spring容器加载时注入,会临时注入一个代理对象,等真正使用的时候再通过代理对象去调用最终的getBean()方法
  2. 添加一个中间类, 中间类去依赖A\B, 然后让中间类去组织他们的依赖方法

SpringBoot开启循环依赖

不建议开启,能产生循环依赖问题的代码本身就是一种不规范的设计。

Spring作者都已表示可能会在后续版本去掉循环依赖支持。 除非你是把 Spring代码移植到SpringBoot ,可以考虑开启循环依赖已保证之前代码正常性。

properties 复制代码
spring.main.allow-circular-references=true
相关推荐
Passion不晚几秒前
Java 全栈开发中的最佳实践:从前端到后端的完整流程
java·前端·全栈
THE WHY8 分钟前
【systemctl start jenkins】启动报错问题解决
java·运维·ci/cd·jenkins
敲代码的小王!1 小时前
mybatisplus介绍以及使用(下)
java·数据库·mybatis
卡卡卡卡罗特2 小时前
naocs注册中心,配置管理,openfeign在idea中实现模块间的调用,getway的使用
java·开发语言
星迹日2 小时前
Java: 数据类型与变量和运算符
java·开发语言·经验分享·笔记
南浔Pyer3 小时前
AI驱动的Java开发框架:Spring AI Alibaba实战部署教程
java·人工智能·spring boot·spring·maven·idea
神的孩子都在歌唱5 小时前
行为设计模式 -模板方法模式- JAVA
java·设计模式·模板方法模式
Satan7127 小时前
【Java】虚拟机(JVM)内存模型全解析
java·开发语言·jvm
远望樱花兔8 小时前
【d54_2】【Java】【力扣】142.环形链表
java·leetcode·链表
IT学长编程8 小时前
计算机毕业设计 助农产品采购平台的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·助农产品采购平台