Spring如何解决循环依赖

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

  1. 调用 getBean(RoleService)
  2. 实例化 RoleService(通过构造函数new RoleService()),此时它的 userService 字段为 null。我们得到一个原始对象。
  3. Spring将一个能获取RoleService早期引用的 ObjectFactory 工厂对象放入第三级缓存,同时从二级缓存中删除任何旧的 RoleService 引用。此时一、二级缓存都没有RoleService
  4. 开始为 RoleService 注入属性(populateBean)。发现它需要 userService

第2步:为 RoleService 寻找依赖 userService

  1. 调用 getBean(UserService)
  2. 实例化 UserServicenew UserService()),此时它的 roleService 字段为 null。得到一个原始对象。
  3. Spring发现 UserService@Transactional 注解,它最终需要是一个代理对象。Spring将一个能处理 UserServiceObjectFactory 工厂对象放入第三级缓存。这个工厂的逻辑是:"如果是我第一次被调用,我会判断这个Bean是否需要代理,如果需要,我就创建一个代理对象返回;如果不需要,我就返回原始对象。"
  4. 开始为 UserService 注入属性。发现它需要 roleService

第3步:为 UserService 寻找依赖 roleService

  1. 调用 getBean(RoleService)
  2. Spring首先在一级缓存找,没有。
  3. 然后在二级缓存找,也没有。
  4. 最后在第三级缓存中找到了之前存放的 RoleServiceObjectFactory
  5. 调用这个工厂的 getObject() 方法。由于 RoleService 没有AOP切面,工厂直接返回原始的 RoleService 早期引用。
  6. Spring将这个从三级缓存取出的原始引用放入二级缓存(以免下次再调用工厂),并从三级缓存中移除该工厂。
  7. 现在,Spring拿到了 roleService 的早期引用,并将其注入到 userService 对象中。
  8. UserService 属性注入完成,继续进行后续初始化(如 @PostConstruct)。

第4步:完成 UserService 的创建

  1. UserService 初始化完成后,就来到真正代理的地方------Bean初始化完成后。Spring需要检查它: 这个Bean在生命早期被暴露出去时(通过三级缓存),是不是已经被代理过了?
  2. 在这个例子中,并没有 ,即第三级缓存中还有这个UserService的Bean的工厂,工厂还没被执行过。这意味着在之前的循环依赖中,没有其他 Bean依赖这个代理对象。
  3. 因此,Spring就会为 UserService 创建代理对象。最终放入一级缓存的不是原始的 UserService,而是它的代理对象 UserServiceProxy,并清理二、三级缓存中关于它的数据。

第5步:回溯,完成 RoleService 的创建

  1. 现在,Spring回溯到第1步,继续为 RoleService 注入属性。
  2. 它现在有了完整的 userService 依赖,但这个依赖是 UserServiceProxy(来自一级缓存)。
  3. Spring将 UserServiceProxy 注入到 RoleServiceuserService 字段中。
  4. RoleService 属性注入完成,进行后续初始化。
  5. 初始化完成后,Spring发现它没有需要代理的地方,于是直接将这个原始的 RoleService 对象放入一级缓存,并清理二、三级缓存中关于它的数据。

其实我这里例子的初始化顺序不太好,应该变成 UserService 先创建能更好说明,但也差不多,其步骤大概如下:

第1步:开始创建 UserService (需要代理的 Bean)

  1. Spring 调用 getBean(UserService),开始创建流程。
  2. 实例化:通过构造函数 new UserService(),得到一个原始对象。此时所有字段为 null
  3. Spring 立即向第三级缓存 (singletonFactories) 注册一个 ObjectFactory 工厂对象。这个工厂对象封装了一个关键逻辑: "当调用我的 getObject() 方法时,我会去执行所有后处理器的 getEarlyBeanReference 方法,对于 UserService,这会返回一个 AOP 代理对象。"
  4. 开始属性填充 (populateBean),发现需要注入 roleService

第2步:为 UserService 寻找依赖 roleService

  1. 调用 getBean(RoleService)
  2. 发现 RoleService 不存在,开始创建 RoleService

第3步:开始创建 RoleService (普通 Bean)

  1. 实例化:通过构造函数 new RoleService(),得到一个原始对象。
  2. Spring 向第三级缓存注册一个用于 RoleServiceObjectFactory。由于 RoleService 是普通 Bean,这个工厂被调用时只会返回原始对象。
  3. 开始属性填充,发现需要注入 userService

第4步:为 RoleService 寻找依赖 userService(触发第三级缓存工厂)

  1. 调用 getBean(UserService),尝试获取 userService
  2. 查找一级缓存 (singletonObjects),没有(因为 UserService 还没创建完)。
  3. 查找二级缓存 (earlySingletonObjects),没有。
  4. 查找第三级缓存 (singletonFactories),能找到! 这就是第1步为 UserService 注册的那个工厂。
  5. Spring 调用这个工厂的 getObject() 方法。
  6. 工厂方法执行,内部的 AbstractAutoProxyCreator 等后处理器开始工作。它们检查 UserService,发现它有 @Transactional 注解,判定它需要被代理。
  7. 工厂方法因此不会返回原始的 UserService 对象,而是动态生成并返回一个 UserService 的代理对象(例如 UserServiceProxy)。
  8. Spring 拿到这个 UserServiceProxy 后,立即将其从第三级缓存升级到第二级缓存 (earlySingletonObjects) 中,并从三级缓存中移除该工厂。注意,这一步至关重要,它记录了这个代理对象已经被暴露过了。
  9. 现在,Spring 拿到了 userService 的依赖------一个代理对象 UserServiceProxy,并将其注入到 RoleService 的对应字段中。
  10. RoleService 的属性填充完成,继续进行后续初始化(@PostConstruct 等)。
  11. RoleService 初始化完成,成为一个完整的 Bean,被放入一级缓存 (singletonObjects) 。

第5步:回溯,继续完成 UserService 的创建

  1. 现在,Spring 回溯到第1步 UserService 的属性填充阶段。它现在有了完整的 roleService 依赖(刚从一级缓存获取)。
  2. Spring 将完整的 roleService Bean 注入到 UserService(原始对象)的字段中。
  3. UserService 属性填充完成,进行后续初始化。
  4. 初始化完成后,Spring 需要执行最后一步:检查这个 Bean 是否需要进行 AOP 代理。它做了一个关键检查:这个 Bean 在早期是否已经被暴露过?(即它的工厂是否已经被调用过并生成了代理?)
  5. 检查发现:是的啊! 它的早期引用(代理对象 UserServiceProxy)已经存在于二级缓存中。这意味着在之前的循环依赖中,已经有其他 Bean(RoleService)依赖了这个代理对象。
  6. 为了保证容器中所有对 UserService 的引用都是同一个对象 ,Spring 不会再次创建代理 。它会直接获取二级缓存中的那个 UserServiceProxy。(注意,从二级缓存取出来的代理对象,它包裹的那个原始对象就是当前的Bean)
  7. 将最终的 UserServiceProxy 放入一级缓存,完成 UserService 的创建。

2.3 如果没有第三级缓存,只有二级缓存会怎样?

假设我们删掉第三级缓存,只在实例化后向二级缓存暴露原始对象。

  1. 在第3步,为 UserServiceroleService 时,会直接从二级缓存拿到原始的 RoleService 引用,没问题。
  2. 在第4步,完成 UserService 创建时,Spring发现它需要代理,于是生成了 UserServiceProxy 放入一级缓存。
  3. 问题出现了:RoleService 中注入的 userService 是第二步中从二级缓存拿到的那个原始 UserService 对象,而不是最终存放在一级缓存里的 UserServiceProxy
  4. 这就导致了严重的不一致RoleService 持有一个没有被代理的、无效的 UserService,它上面的 @Transactional 等注解全部失效。这是一个致命的错误。

其实,如果没有代理,那么二级缓存就足以解决循环依赖,但是AOP可是Spring的一大特性,怎么可能没有代理,所以第三级缓存是必不可少的!

第三级缓存存的不是对象本身,而是一个工厂。这个工厂的妙处在于:

  • 它把"生成早期引用"这个动作延迟到了真正需要的时候(即发生循环依赖时)。
  • 对于需要代理的Bean(如 UserService),这个工厂会返回代理对象。
  • 对于普通Bean(如 RoleService),这个工厂会返回原始对象。

这样,当 RoleService 通过工厂获取 UserService 的早期引用时,拿到的是已经被代理的 UserServiceProxy。然后这个代理对象会被存入二级缓存。

之后,当 UserService 完成初始化时,Spring会发现:"哦,这个Bean的早期代理对象已经被提前暴露过了(存在于二级缓存中)",它就不会再次创建代理,而是直接使用二级缓存里的那个代理对象,保证了整个容器中引用的唯一性和正确性。

3、解决循环依赖的条件

要想Spring能自动处理循环依赖,必须满足下面的两点条件:

  1. 依赖的Bean必须都是单例;
  2. 依赖注入的方式,不能全是构造器注入,并且先注册Beandefinition的那个Bean不能是构造器注入。

3.1 为什么必须都是单例

AbstractBeanFactorydoGetBean 方法中,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来验证它们的注册顺序,BeanFactoryPostProcessorpostProcessBeanFactory 方法就会在所有 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);
        }
    }
}
相关推荐
Jacobshash15 小时前
SpringCloud框架组件梳理
后端·spring·spring cloud
孤狼程序员15 小时前
深入探讨Java异常处理:受检异常与非受检异常的最佳实践
java·后端·spring
3Cloudream1 天前
互联网大厂Java面试:从基础到微服务的深度解析
java·spring·微服务·面试·技术解析·电商场景
C++chaofan1 天前
Spring Task快速上手
java·jvm·数据库·spring boot·后端·spring·mybatis
yinke小琪1 天前
Spring生态全家桶:从基础到微服务的演进与关联是什么?
java·后端·spring
一枚小小程序员哈1 天前
基于微信小程序的诊所信息系统的设计与实现
spring boot·struts·spring·spring cloud·java-ee·maven·intellij-idea
杨杨杨大侠2 天前
手搓 Log 框架:Atlas Log 系统架构
java·spring·apache log4j
麦兜*2 天前
大模型时代:用Redis构建百亿级向量数据库方
数据库·spring boot·redis·spring·spring cloud·缓存
the beard2 天前
Feign整合Sentinel实现服务降级与Feign拦截器实战指南
java·spring·sentinel