文章目录

一、引言:Spring 为什么要解决循环依赖?

在 Spring 中,Bean 之间通过依赖注入(DI)彼此引用是非常常见的事情。但当出现这种情况:

  • A 依赖 B
  • B 又依赖 A

就构成了 循环依赖(Circular Dependency)

如果容器没能力处理这种情况,稍微复杂一点的业务系统就会在启动时疯狂报错。
所以 Spring 专门设计了一套 “三级缓存 + 提前暴露 Bean 引用” 的机制,来解决一部分循环依赖问题。


二、Spring 能解决/不能解决哪些循环依赖?

1、Spring 能自动解决的

仅限下面这一类:

  • 作用域:singleton(单例)
  • 注入方式:字段注入 / Setter 注入(非构造器)
  • 容器允许循环依赖:allowCircularReferences = true(Spring Boot 3 之后默认为 false,需要手动打开)

典型例子:

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

这种 单例 + 属性注入 的循环依赖,Spring 能自动帮你解开。


2、Spring 无法自动解决的

(1)构造器注入循环依赖

@Component
public class A {
    private final B b;
    public A(B b) { this.b = b; }
}

@Component
public class B {
    private final A a;
    public B(A a) { this.a = a; }
}

创建 A 需要先有 B,创建 B 又需要先有 A,实例都 new 不出来,Spring 没法“提前暴露半成品”,只能抛异常:

BeanCurrentlyInCreationException

(2)prototype 作用域循环依赖

@Scope("prototype")
@Component
public class A {
    @Autowired
    private B b;
}

@Scope("prototype")
@Component
public class B {
    @Autowired
    private A a;
}

prototype 每次 getBean() 都新建一个对象,Spring 不会对 prototype 用单例那套缓存标记/提前暴露机制,所以也没法解决循环依赖。


三、Bean 创建的大致流程

1、入口:getBean() → doGetBean()

精简后的伪代码(非完整源码):

protected Object doGetBean(String name) {
    // 1. 尝试从单例缓存中获取(包括早期引用)
    Object sharedInstance = getSingleton(name);
    if (sharedInstance != null) {
        return sharedInstance;
    }

    // 2. 解析 BeanDefinition
    RootBeanDefinition mbd = getMergedBeanDefinition(name);

    // 3. 如果是单例,则通过 getSingleton(name, ObjectFactory) 创建
    if (mbd.isSingleton()) {
        Object bean = getSingleton(name, () -> createBean(name, mbd));
        return bean;
    }

    // 其他 scope 略
}

这里有两个关键的 getSingleton

  • getSingleton(String name):只负责从缓存中拿
  • getSingleton(String name, ObjectFactory)不存在就创建(里面会调用 ObjectFactory.getObject(),实际走到 createBean()doCreateBean()

2、创建单例:getSingleton(name, ObjectFactory)(外层壳)

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Object singletonObject = singletonObjects.get(beanName);
    if (singletonObject == null) {
        // 标记这个 Bean 正在创建中
        beforeSingletonCreation(beanName);
        try {
            // ✨ 真正创建 Bean(调用 createBean → doCreateBean)
            singletonObject = singletonFactory.getObject();
            // 创建完成后放入一级缓存
            addSingleton(beanName, singletonObject);
        } finally {
            afterSingletonCreation(beanName);
        }
    }
    return singletonObject;
}

真正的重头戏createBean()doCreateBean(),以及我们接下来要讲的三级缓存。


四、三级缓存详解:Spring 到底在内存里放了什么?

Spring 为了管理单例 Bean,维护了 3 个重要的 Map(都在 DefaultSingletonBeanRegistry 中):

1、一级缓存:singletonObjects —— 成品 Bean

/** beanName -> 完全初始化好的 Bean 实例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

特点:

  • 存放的是完全初始化完成的 Bean:

    • 构造器调用完成
    • 依赖注入完成
    • 各种初始化回调(afterPropertiesSetinit-method 等)都执行完
    • 如果有 AOP,通常已经是代理对象 proxyA
  • 你平时 applicationContext.getBean("a") 最终拿到的就是这里面的对象


2、二级缓存:earlySingletonObjects —— 提前暴露的早期引用

/** beanName -> 早期曝光的 Bean 引用(可能是半成品) */
private final Map<String, Object> earlySingletonObjects = new HashMap<>();

特点:

  • 存放 “早期曝光的 Bean 引用”,也就是半成品 Bean
  • 用于在循环依赖场景下,让另一个 Bean 先用上这个“正在创建中的 Bean”

3、三级缓存:singletonFactories —— 生成早期引用的工厂

/** beanName -> ObjectFactory,用于生成该 Bean 的早期引用 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();

特点:

  • 存的不是 Bean 对象,而是一个 ObjectFactory 工厂

  • 这个工厂的典型形式:

    () -> getEarlyBeanReference(beanName, rawBeanInstance);
    
  • 真正发生循环依赖,需要提前引用这个 Bean 时,才会调用工厂 getObject()

    • 无 AOP:返回 rawA
    • 有 AOP:返回 proxyA(代理对象)

4、三层缓存的流转顺序

一句话:

三级缓存存工厂 → 需要时工厂生成“早期引用”放入二级缓存 → Bean 完成创建后进入一级缓存。


五、关键方法:getSingleton(name) 如何用三层缓存拿 Bean?

getSingleton(String beanName) 专门负责从缓存里取 Bean,是循环依赖时的关键。

简化伪代码:

public Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 先从一级缓存拿(成品 Bean)
    Object singletonObject = singletonObjects.get(beanName);

    // 2. 一级没有,且该 Bean 正在创建中,才考虑早期引用
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (singletonObjects) {
            // 2.1 再看二级缓存(早期引用)
            singletonObject = earlySingletonObjects.get(beanName);

            // 2.2 二级也没有,而且允许提前引用,才去动用三级缓存
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 通过工厂生成早期引用(可能是代理对象)
                    singletonObject = singletonFactory.getObject();
                    // 缓存到二级
                    earlySingletonObjects.put(beanName, singletonObject);
                    // 从三级缓存中移除工厂
                    singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

关键点:

  • 正常情况下:直接从一级缓存拿成品 Bean
  • 正在创建中:才会考虑去二级/三级找早期引用
  • 第一次用到三级缓存时,会立刻“下沉”为二级缓存(工厂用完就删)

六、doCreateBean:在哪一步把 Bean 提前暴露出去?

Spring 真正创建 Bean 的逻辑在 AbstractAutowireCapableBeanFactory#doCreateBean

简化伪代码:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
    // 1. 实例化(只调用构造器,不注入属性)
    Object beanInstance = createBeanInstance(beanName, mbd, args);

    // 2. 是否需要提前暴露(单例 & 允许循环依赖)
    boolean earlySingletonExposure = mbd.isSingleton() && allowCircularReferences && isSingletonCurrentlyInCreation(beanName);
    if (earlySingletonExposure) {
        // ✨ 关键:往三级缓存注册一个 ObjectFactory
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, beanInstance));
    }

    // 3. 属性填充(@Autowired 等)
    populateBean(beanName, mbd, beanInstance);

    // 4. 初始化(调用各种 BeanPostProcessor、init-method等)
    Object exposedObject = initializeBean(beanName, beanInstance, mbd);

    return exposedObject;
}

注意:

  • addSingletonFactory 放的是“将来如何拿早期引用”的工厂,而不是直接把 Bean 丢进二级缓存
  • 真正发生循环依赖时,才会触发三级缓存 → 工厂 → 二级缓存的流程

七、A ↔ B 字段注入循环依赖:源码级时序走一遍

假设:

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

我们按调用顺序看一遍:

1、容器开始创建 A:getBean(“a”)

  • 调用 doGetBean("a")
  • 单例 → 调用两参版 getSingleton("a", ObjectFactory)
  • 一级缓存中没有 → 调用 singletonFactory.getObject() → 进入 createBean("a")doCreateBean("a")

2、doCreateBean(“a”) 内部

  1. createBeanInstance("a")

    rawA = new A();  // 只构造,不注入 B
    
  2. addSingletonFactory("a", () -> getEarlyBeanReference("a", rawA));
    👉 A 的早期工厂进入 三级缓存

  3. populateBean("a"):给 A 注入依赖

    • 发现需要 B → 调用 getBean("b")

3、开始创建 B:getBean(“b”)

  • 同样走到 getSingleton("b", ObjectFactory)doCreateBean("b")

doCreateBean("b") 里:

  1. createBeanInstance("b")

    rawB = new B();
    
  2. addSingletonFactory("b", () -> getEarlyBeanReference("b", rawB));

  3. populateBean("b")

    • 发现字段 A a,需要 getBean("a")

4、创建 B 的过程中再次请求 A:getBean(“a”) → getSingleton(“a”)

现在 A 已经在“创建中”了,getSingleton("a") 的行为是:

  1. 一级缓存:singletonObjects["a"] → 没有(A 未完成)

  2. 检测到 A 正在创建中 → 可以尝试早期引用

  3. 二级缓存:earlySingletonObjects["a"] → 还没有

  4. 三级缓存:

    • 取出 singletonFactories["a"] 中的 ObjectFactory

    • 调用 getObject() → 内部执行:

      earlyA = getEarlyBeanReference("a", rawA);
      
      • 如果无 AOP:通常是 return rawA;
      • 如果有 @Transactional / 切面:这里可能返回 proxyA
    • earlyA 放进二级缓存:

      earlySingletonObjects["a"] = earlyA;
      singletonFactories.remove("a");
      

于是:

  • B 在属性注入时,拿到的是 earlyA,也就是 A 的早期引用(可能已是代理)

5、B 完成创建 → 放入一级缓存

B 剩余流程:

  • 继续 populateBean("b"),注入完成

  • 调用 initializeBean("b")(执行初始化回调、BeanPostProcessor)

  • 得到完整体 fullB,放入一级缓存:

    singletonObjects["b"] = fullB;
    

6、回到 A 的创建现场 → 完成 A

  • 控制流回到 populateBean("a") 后面

  • 给 A 注入 B 时调用 getBean("b")

    • 此时 B 已在一级缓存中,直接拿到 fullB
  • 执行 initializeBean("a")

  • 得到 fullA,加入一级缓存:

    singletonObjects["a"] = fullA;
    earlySingletonObjects.remove("a");
    

循环依赖成功解除。


八、为什么必须是“三级缓存”?两级缓存为什么不够?

要点只有一个:AOP / 事务代理造成的“rawA vs proxyA 区别”。

1、只有两级缓存会怎样?

假设只有:

  • 一级:singletonObjects(成品 Bean)
  • 二级:earlySingletonObjects(早期 Bean)

doCreateBean("a") 时,只能这么做:

rawA = new A();
earlySingletonObjects["a"] = rawA;  // 为了解决循环依赖

然后再执行 AOP / BeanPostProcessor:

proxyA = wrapIfNecessary(rawA);
singletonObjects["a"] = proxyA;

结果:

  • B 在创建过程中注入的是 rawA(没事务、没切面)
  • 其他 Bean 以后从容器拿的是 proxyA(有事务、切面)

👉 同一个 Bean,在容器里出现两种不同形态

  • 有的地方事务生效
  • 有的地方事务不生效

严重违背了容器的一致性原则。


2、三级缓存的解决方案

三级缓存存的是一个“工厂”:

addSingletonFactory("a", () -> getEarlyBeanReference("a", rawA));
  • 此时 并不立即暴露 rawA
  • 只是先把“以后怎么拿早期引用”的能力存起来

当真正发生循环依赖,需要提前暴露 A 时:

ObjectFactory<?> factory = singletonFactories["a"];
earlyA = factory.getObject();  // 内部可能返回 proxyA
earlySingletonObjects["a"] = earlyA;
singletonFactories.remove("a");

getEarlyBeanReference() 中可以:

  • 根据是否有 AOP / 事务,决定是否生成代理
  • 保证所有地方看到的早期引用都是同一份对象(通常是 proxyA

一句话总结这一节:

二级缓存只能存“对象结果”,三级缓存存的是“生成对象的工厂”,可以延迟到真正需要提前暴露时再做“要不要代理”的决策,从而保证容器内对同一 Bean 的引用形态一致。


九、为什么构造器 / prototype 循环依赖解决不了?

1、构造器循环依赖:实例都 new 不出来,谈什么提前暴露

构造器注入要求:

new A(b);
new B(a);

但刚才我们看到:

  • 只有 createBeanInstance(beanName) 之后,Spring 才有 rawA

  • 然后才会:

    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, rawA));
    

构造器循环依赖中:

  • 为了实例化 A,需要先准备 B
  • 为了实例化 B,需要先准备 A
  • A / B 都还没 new 出来,更别说注册三级缓存工厂

👉 所以构造器循环依赖在时序上 根本没有“半成品可以提前暴露”这一刻,Spring 只能报错。


2、prototype 不走单例缓存那一套

DefaultSingletonBeanRegistry 这套:

  • singletonObjects
  • earlySingletonObjects
  • singletonFactories
  • isSingletonCurrentlyInCreation

都是服务于单例 Bean 的。

prototype 每次 getBean() 都新建:

  • 不会进入这三层缓存
  • 不会被标记为“currently in creation”
  • 不会注册 ObjectFactory 用于提前暴露

所以:

prototype Bean 一旦循环依赖,没有任何“提前暴露半成品”的机会,Spring 自然也就解不了环。


十、AOP 场景下的 rawA / proxyA:为什么 Spring 拼命想让大家都拿到 proxyA?

再用一个例子感受下 rawA vs proxyA

@Service
public class UserService {

    @Transactional
    public void createUser() {
        // 插入数据库
    }
}

Spring 实际上做了两件事:

  1. new UserService() → 得到 rawUserService(rawA)
  2. 判断有 @Transactional → 用代理包装成 proxyUserService(proxyA)

调用:

  • proxyUserService.createUser() → 会先进入代理逻辑:

    • 开启事务
    • 调用 rawUserService.createUser()
    • 提交/回滚事务
  • 若绕过代理直接调 rawUserService.createUser()没有事务!

👉 所以容器必须保证:

能被外面拿到并使用的,尽量都是 proxyA,而不是 rawA

而三级缓存中的 getEarlyBeanReferenceObjectFactory,就是为了在循环依赖场景下,也尽可能把 proxyA(代理对象) 暴露给其他 Bean,而不是原始的 rawA。


十一、实战建议

1、实战建议

  • 优先从设计上避免循环依赖

    • 抽公共 Service / Manager
    • 使用事件、回调、观察者等模式解耦
  • 确实改不动时:

    • 保证两端都是 singleton
    • 使用字段 / Setter 注入,而非构造器注入
    • 必要时使用 @Lazy 打破强实时依赖
  • Spring Boot 3 / Spring 6 以后:

    spring:
      main:
        allow-circular-references: true
    

    如果你需要沿用 Spring 的自动解环能力,需要显式开启。


2、总结

Spring 只能自动解决 singleton + 字段/Setter 注入 场景下的循环依赖,核心是 DefaultSingletonBeanRegistry 中的三级缓存机制。

  • 一级缓存 singletonObjects 存放完全初始化好的单例 Bean;
  • 二级缓存 earlySingletonObjects 存放提前暴露的早期引用;
  • 三级缓存 singletonFactories 存放一个 ObjectFactory,用于在真正发生循环依赖时,通过 getEarlyBeanReference 生成早期引用(可能是 AOP 代理 proxyA)。

doCreateBean 中,Spring 会在实例化 Bean 后、属性注入前,把这个工厂放进三级缓存;当另一个 Bean 注入它时,通过 getSingleton 从三级缓存得到早期引用并放入二级缓存,从而打破 A ↔ B 的循环。

由于构造器注入在实例化前就需要依赖对象,prototype 又不使用这套单例缓存机制,所以 构造器循环依赖和 prototype 循环依赖是无法由 Spring 自动解决的

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐