1、什么是循环依赖
循环依赖是指两个或多个Bean相互依赖,形成一个闭环。例如:
java
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
在这个例子中,A依赖B,B又依赖A,形成了一个循环依赖链。
2、如何解决循环依赖
先说结论:Spring 使用三级缓存(其实就是三个Map
)来解决循环依赖的问题,同时确保 Bean 的完整性和单例的正确性。
缓存级别 | 存储内容 | 作用阶段 | 解决的核心问题 |
---|---|---|---|
一级缓存 singletonObjects |
存储完全初始化好的、成熟的单例Bean,即"成品"库。 | Bean 初始化完成 | 提供最终可用的 Bean |
二级缓存 earlySingletonObjects |
存储从三级缓存中取出的、经过处理的早期引用(主要是处理过AOP代理的)。是已实例化但未属性注入的半成品 Bean,即"半成品"临时库。 | Bean 实例化后,属性注入前 | 提前暴露引用,解决循环依赖 |
三级缓存 singletonFactories |
存储一个 ObjectFactory 工厂对象。这个工厂能够返回一个Bean的早期引用(原始对象或代理对象),即"工厂"库。 |
Bean 实例化后立即存入 | 处理 AOP 代理对象的特殊场景 |
特别注意第三级缓存,它是一个能够执行"后处理"的工厂,它决定了暴露出去的早期引用是原始对象还是代理对象。
现在通过一个具体的、逐步的例子来深入理解Spring三级缓存的作用,特别是第三级缓存的必要性。
2.1 场景设定
我们有两个相互依赖的Service,并且 UserService
被AOP代理(比如用了 @Transactional
或者@Async
注解)。
java
@Service
public class RoleService {
@Autowired
private UserService userService; // 依赖了一个需要被代理的Bean
}
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional // 这个注解使得UserService最终需要是一个代理对象
public void someMethod() {
// ...
}
}
Spring容器启动时,需要解决这个循环依赖。我们重点关注第三级缓存 singletonFactories
的作用。
2.2 解决循环依赖的详细步骤
第1步:开始创建 RoleService
- 调用
getBean(RoleService)
。 - 实例化
RoleService
(通过构造函数new RoleService()
),此时它的userService
字段为null
。我们得到一个原始对象。 - Spring将一个能获取
RoleService
早期引用的ObjectFactory
工厂对象放入第三级缓存,同时从二级缓存中删除任何旧的RoleService
引用。此时一、二级缓存都没有RoleService
。 - 开始为
RoleService
注入属性(populateBean
)。发现它需要userService
。
第2步:为 RoleService
寻找依赖 userService
- 调用
getBean(UserService)
。 - 实例化
UserService
(new UserService()
),此时它的roleService
字段为null
。得到一个原始对象。 - Spring发现
UserService
有@Transactional
注解,它最终需要是一个代理对象。Spring将一个能处理UserService
的ObjectFactory
工厂对象放入第三级缓存。这个工厂的逻辑是:"如果是我第一次被调用,我会判断这个Bean是否需要代理,如果需要,我就创建一个代理对象返回;如果不需要,我就返回原始对象。" - 开始为
UserService
注入属性。发现它需要roleService
。
第3步:为 UserService
寻找依赖 roleService
- 调用
getBean(RoleService)
。 - Spring首先在一级缓存找,没有。
- 然后在二级缓存找,也没有。
- 最后在第三级缓存中找到了之前存放的
RoleService
的ObjectFactory
。 - 调用这个工厂的
getObject()
方法。由于RoleService
没有AOP切面,工厂直接返回原始的RoleService
早期引用。 - Spring将这个从三级缓存取出的原始引用放入二级缓存(以免下次再调用工厂),并从三级缓存中移除该工厂。
- 现在,Spring拿到了
roleService
的早期引用,并将其注入到userService
对象中。 UserService
属性注入完成,继续进行后续初始化(如@PostConstruct
)。
第4步:完成 UserService
的创建
UserService
初始化完成后,就来到真正代理的地方------Bean初始化完成后。Spring需要检查它: 这个Bean在生命早期被暴露出去时(通过三级缓存),是不是已经被代理过了?- 在这个例子中,并没有 ,即第三级缓存中还有这个
UserService
的Bean的工厂,工厂还没被执行过。这意味着在之前的循环依赖中,没有其他 Bean依赖这个代理对象。 - 因此,Spring就会为
UserService
创建代理对象。最终放入一级缓存的不是原始的UserService
,而是它的代理对象UserServiceProxy
,并清理二、三级缓存中关于它的数据。
第5步:回溯,完成 RoleService
的创建
- 现在,Spring回溯到第1步,继续为
RoleService
注入属性。 - 它现在有了完整的
userService
依赖,但这个依赖是UserServiceProxy
(来自一级缓存)。 - Spring将
UserServiceProxy
注入到RoleService
的userService
字段中。 RoleService
属性注入完成,进行后续初始化。- 初始化完成后,Spring发现它没有需要代理的地方,于是直接将这个原始的
RoleService
对象放入一级缓存,并清理二、三级缓存中关于它的数据。
其实我这里例子的初始化顺序不太好,应该变成 UserService
先创建能更好说明,但也差不多,其步骤大概如下:
第1步:开始创建 UserService
(需要代理的 Bean)
- Spring 调用
getBean(UserService)
,开始创建流程。 - 实例化:通过构造函数
new UserService()
,得到一个原始对象。此时所有字段为null
。 - Spring 立即向第三级缓存 (
singletonFactories
) 注册一个ObjectFactory
工厂对象。这个工厂对象封装了一个关键逻辑: "当调用我的getObject()
方法时,我会去执行所有后处理器的getEarlyBeanReference
方法,对于UserService
,这会返回一个 AOP 代理对象。" - 开始属性填充 (
populateBean
),发现需要注入roleService
。
第2步:为 UserService
寻找依赖 roleService
- 调用
getBean(RoleService)
。 - 发现
RoleService
不存在,开始创建RoleService
。
第3步:开始创建 RoleService
(普通 Bean)
- 实例化:通过构造函数
new RoleService()
,得到一个原始对象。 - Spring 向第三级缓存注册一个用于
RoleService
的ObjectFactory
。由于RoleService
是普通 Bean,这个工厂被调用时只会返回原始对象。 - 开始属性填充,发现需要注入
userService
。
第4步:为 RoleService
寻找依赖 userService
(触发第三级缓存工厂)
- 调用
getBean(UserService)
,尝试获取userService
。 - 查找一级缓存 (
singletonObjects
),没有(因为UserService
还没创建完)。 - 查找二级缓存 (
earlySingletonObjects
),没有。 - 查找第三级缓存 (
singletonFactories
),能找到! 这就是第1步为UserService
注册的那个工厂。 - Spring 调用这个工厂的
getObject()
方法。 - 工厂方法执行,内部的
AbstractAutoProxyCreator
等后处理器开始工作。它们检查UserService
,发现它有@Transactional
注解,判定它需要被代理。 - 工厂方法因此不会返回原始的
UserService
对象,而是动态生成并返回一个UserService
的代理对象(例如UserServiceProxy
)。 - Spring 拿到这个
UserServiceProxy
后,立即将其从第三级缓存升级到第二级缓存 (earlySingletonObjects
) 中,并从三级缓存中移除该工厂。注意,这一步至关重要,它记录了这个代理对象已经被暴露过了。 - 现在,Spring 拿到了
userService
的依赖------一个代理对象UserServiceProxy
,并将其注入到RoleService
的对应字段中。 RoleService
的属性填充完成,继续进行后续初始化(@PostConstruct
等)。RoleService
初始化完成,成为一个完整的 Bean,被放入一级缓存 (singletonObjects
) 。
第5步:回溯,继续完成 UserService
的创建
- 现在,Spring 回溯到第1步
UserService
的属性填充阶段。它现在有了完整的roleService
依赖(刚从一级缓存获取)。 - Spring 将完整的
roleService
Bean 注入到UserService
(原始对象)的字段中。 UserService
属性填充完成,进行后续初始化。- 初始化完成后,Spring 需要执行最后一步:检查这个 Bean 是否需要进行 AOP 代理。它做了一个关键检查:这个 Bean 在早期是否已经被暴露过?(即它的工厂是否已经被调用过并生成了代理?)
- 检查发现:是的啊! 它的早期引用(代理对象
UserServiceProxy
)已经存在于二级缓存中。这意味着在之前的循环依赖中,已经有其他 Bean(RoleService
)依赖了这个代理对象。 - 为了保证容器中所有对
UserService
的引用都是同一个对象 ,Spring 不会再次创建代理 。它会直接获取二级缓存中的那个UserServiceProxy
。(注意,从二级缓存取出来的代理对象,它包裹的那个原始对象就是当前的Bean) - 将最终的
UserServiceProxy
放入一级缓存,完成UserService
的创建。
2.3 如果没有第三级缓存,只有二级缓存会怎样?
假设我们删掉第三级缓存,只在实例化后向二级缓存暴露原始对象。
- 在第3步,为
UserService
找roleService
时,会直接从二级缓存拿到原始的RoleService
引用,没问题。 - 在第4步,完成
UserService
创建时,Spring发现它需要代理,于是生成了UserServiceProxy
放入一级缓存。 - 问题出现了:
RoleService
中注入的userService
是第二步中从二级缓存拿到的那个原始UserService
对象,而不是最终存放在一级缓存里的UserServiceProxy
。 - 这就导致了严重的不一致 :
RoleService
持有一个没有被代理的、无效的UserService
,它上面的@Transactional
等注解全部失效。这是一个致命的错误。
其实,如果没有代理,那么二级缓存就足以解决循环依赖,但是AOP可是Spring的一大特性,怎么可能没有代理,所以第三级缓存是必不可少的!
第三级缓存存的不是对象本身,而是一个工厂。这个工厂的妙处在于:
- 它把"生成早期引用"这个动作延迟到了真正需要的时候(即发生循环依赖时)。
- 对于需要代理的Bean(如
UserService
),这个工厂会返回代理对象。 - 对于普通Bean(如
RoleService
),这个工厂会返回原始对象。
这样,当 RoleService
通过工厂获取 UserService
的早期引用时,拿到的是已经被代理的 UserServiceProxy
。然后这个代理对象会被存入二级缓存。
之后,当 UserService
完成初始化时,Spring会发现:"哦,这个Bean的早期代理对象已经被提前暴露过了(存在于二级缓存中)",它就不会再次创建代理,而是直接使用二级缓存里的那个代理对象,保证了整个容器中引用的唯一性和正确性。
3、解决循环依赖的条件
要想Spring能自动处理循环依赖,必须满足下面的两点条件:
- 依赖的Bean必须都是单例;
- 依赖注入的方式,不能全是构造器注入,并且先注册
Beandefinition
的那个Bean不能是构造器注入。
3.1 为什么必须都是单例
在 AbstractBeanFactory
的 doGetBean
方法中,Spring 会检查 Bean 的作用域。若发现原型 Bean 存在循环依赖,直接抛出异常:
java
// AbstractBeanFactory.java
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
原因也很简单,若允许原型 Bean 的循环依赖,会导致无限递归创建实例(如 A → B → A → B → ...
),最终触发 StackOverflowError
。
3.2 为什么不能全是构造器注入
原因也很简单,如果允许两个构造器注入,那么就会使两个Bean在实例化阶段,都等待对方的工厂方法进入三级缓存后才能继续,就会造成死循环等待。
并且先注册Beandefinition
的那个Bean不能是构造器注入。假设A是构造器注入,且先注册Beandefinition
,那么Spring就会先创建A的Bean,在创建A的Bean时发现依赖B,就转而先去实例化B,此时尽管B依赖的A是set注入,但是在二三级缓存中都没有A的数据,循环也就无法被打破。
至于哪个Bean先被注册Beandefinition
,跟@Bean
方法顺序、@Import
、组件扫描顺序、自动配置等因素有关。不过可以自定义一个BeanFactoryPostProcessor
来验证它们的注册顺序,BeanFactoryPostProcessor
的 postProcessBeanFactory
方法就会在所有 Bean 定义加载完成但尚未实例化时被调用:
java
@Component
public class BeanDefinitionLogger implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 此时所有Bean定义已注册,但未实例化
String[] beanNames = beanFactory.getBeanDefinitionNames();
System.out.println("=== 所有Bean定义名称(按注册顺序)===");
for (String name : beanNames) {
System.out.println(name);
}
}
}