# Spring循环依赖
# 简要回答
- Spring通过三级缓存和提前暴露未完全初始化的对象引用机制来解决单例作用域Bean的循环依赖问题。
- 假设A和B存在循环依赖问题,A创建实例后未注入属性时会存放ObjectFactory对象到三级缓存中,开始给A注入属性时发现A依赖B,此时容器开始创建B,B实例化后也会被存放ObjectFactory对象到三级缓存中,Spring给B注入属性时发现B依赖A,容器在三级缓存中找到A的ObjectFactory对象,获取A的早期引用并放入二级缓存,并清理三级缓存。将A的早期引用注入到B中,完成B的初始化后进入一级缓存。回到A的属性注入环节,将就绪的B注入A,完成A的初始化后进入一级缓存,解决循环依赖问题。
# 详细回答
# 循环依赖
- 循环依赖就是循环引用,有两个或多个Bean之间相互引用,形成闭环,导致容器无法完成Bean的实例化和依赖注入,会抛出循环依赖异常。
- Spring中存在两种循环依赖,分别是构造器依赖和field属性的循环依赖。构造器的循环依赖问题无法解决,只能抛出BeanCurrentlyInCreationException异常,使用提前暴露对象的方法可以解决属性循环依赖。
# 检测循环依赖问题
- 在Bean创建的时候给该Bean打标,当递归回来发现正在创建中的话说明产生了循环依赖。
# 三级缓存解决循环依赖
- 三级缓存是Spring在DefaultSingletonBeanRegistry中维护的三个重要的缓存(Map)
- 一级缓存(singletonObjects) 存放完全创建好的Bean实例,可以直接使用,Bean已经实例化,依赖注入、初始化,有AOP代理也已经生成。
- 二级缓存(earlySingletonObjects) 存放提前暴露的Bean的原始对象引用或者早期代理对象引用,专门用来处理循环依赖。此时Bean已经实例化但是还没有完成依赖注入与初始化。
- 三级缓存(singletonFactories) 存放Bean的ObjectFactory工厂对象,当Bean被实例化后Spring会创建一个工厂对象放入三级缓存。当其他Bean需要获取三级缓存中的Bean时,三级缓存可以动态返回半成品Bean,原始对象或代理对象。
- 当两个Bean存在相互依赖时,Spring的处理过程如下:
- 调用A的构造函数进行实例化,在属性注入之前在第三级缓存(singletonFactories)中创建一个工厂对象,并放入三级缓存中。
- Spring给A注入属性时发现A依赖B,缓存中没有B于是对B进行实例化,实例化过程中同样创建一个工厂对象,并放入三级缓存中。
- Spring给B注入属性时发现B依赖A,同时在三级缓存中找到A的工厂对象,调用该工厂的getObject()方法。该方法在A需要AOP代理时动态生成代理对象,否则返回原始对象,得到的A早期引用或代理对象后存入第二级缓存,同时清除三级缓存中的A工厂对象,使用A的早期引用完成B的依赖注入。
- 随后B正常完成初始化方法并转化为完整可用的Bean,存入一级缓存,同时清除二级缓存或三级缓存中B相关的内容。
- 回到A的依赖注入阶段,此时会直接从二级缓存中获取A的早期引用/代理,在一级缓存中获取B实例完成依赖注入,执行剩余方法A转换为完整可用的Bean,存入一级缓存,同时清除二级缓存或三级缓存中A相关的内容。
# 知识图解
- 循环依赖示意图

# 知识扩展
- 面试官可能追问
- Q1:一定要三级缓存才能解决问题吗?
- 只有三级缓存才能解决循环依赖问题,因为需要正确处理AOP代理的Bean,只使用二级缓存会导致注入对象的形态错误,破坏单例原则。
- 当A和B两个Bean之间存在循环依赖时,如果A需要被动态代理,B创建时会拿到A的原始对象,而不是A的代理对象。导致B持有原始对象A,Spring容器存储的是代理对象A,同一个Bean出现了两个不同的实例,违反了单例的原则。
- Q2:为什么构造器循环依赖不能解决?
- 构造器循环依赖要求在对象实例化的同时就注入依赖,而循环依赖导致这个过程无法启动。而Setter注入和字段注入则是在实例化之后才进行依赖注入,这就为Spring提供了提前暴露Bean实例、解决循环依赖的机会。
- Q3:Spring循环依赖,提前暴露对象的前提是什么?
- Bean是单例作用域且循环依赖是字段属性依赖。
- 其他作用域会造成递归创建,构造器依赖不能提前暴露。
- Q4:Spring循环依赖,实际开发中怎么避免?
- 将产生循环依赖的共同逻辑抽成第三方服务
- 使用@Lazy注解标记依赖,延迟到首次使用时注入
- 使用属性注入替代构造器注入
评论
验证登录状态...