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 将完整的
roleServiceBean 注入到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);
}
}
}