2004年,Rod Johnson在《Expert One-on-One J2EE Development without EJB》一书中首次提出了Spring框架的核心设计理念。其中,依赖注入(Dependency Injection)作为实现控制反转(IoC)的主要手段,彻底改变了Java企业级开发的范式。然而,依赖注入的便利性也带来了一个棘手的问题:当两个或多个Bean相互依赖时,容器该如何处理?

这个问题被称为循环依赖(Circular Dependency)。看似简单,却触及了对象创建与依赖管理的本质矛盾。Spring给出的答案是一套精巧的三级缓存机制——这套机制的设计哲学远比表面看起来更加深邃,它不仅关乎循环依赖的解决,更涉及AOP代理、对象生命周期管理、线程安全等多个维度的权衡。

循环依赖:一个先有鸡还是先有蛋的问题

循环依赖的本质是一个"先有鸡还是先有蛋"的悖论。假设有两个单例Bean A和B:

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

当Spring容器启动时,需要创建这两个Bean。如果按照正常的创建流程:

  1. 创建ServiceA → 发现需要ServiceB → 创建ServiceB
  2. 创建ServiceB → 发现需要ServiceA → 但ServiceA还没创建完成…

这就是死锁的典型场景。在构造器注入的情况下,这种死锁是无法打破的,因为构造函数执行时对象还未创建,无法提前暴露引用。

但Spring选择支持Setter注入和字段注入的循环依赖,核心思路是:在对象实例化完成后、属性注入完成前,就将该对象的引用提前暴露出去。这样,当另一个Bean需要引用它时,即使它还未完全初始化,也能获取到同一个对象引用。

一级缓存的困境:线程安全与性能的两难

最朴素的想法是使用一个缓存来存储所有Bean。但这里有一个关键问题:什么时候将Bean放入缓存?

如果等Bean完全初始化后再放入,循环依赖无法解决(因为另一个Bean在初始化时找不到它)。如果在实例化后立即放入,又会有新的问题:缓存中同时存在完整Bean和半成品Bean,其他线程可能获取到不完整的对象

解决方案只能是加锁。但全局锁意味着每次获取Bean都需要竞争锁,在容器启动后的大量getBean()调用中,性能会严重下降。这是一个典型的"正确性与性能不可兼得"的困境。

二级缓存:分离完整对象与早期对象

Spring的解决方案是引入两级缓存:

  • 一级缓存(singletonObjects):存储完全初始化好的单例Bean
  • 二级缓存(earlySingletonObjects):存储实例化完成但未完成属性注入的早期对象
// DefaultSingletonBeanRegistry中的核心数据结构
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

获取Bean时,先从一级缓存找,找不到再从二级缓存找。由于一级缓存中只有完整对象,正常情况下不需要加锁就能安全获取。

这套方案看似完美,直到AOP登场。

当循环依赖遇上AOP:代理对象的创建时机

Spring AOP通过动态代理(JDK动态代理或CGLIB)为目标对象创建代理。按照正常的设计,代理对象应该在Bean初始化完成后创建,由BeanPostProcessorpostProcessAfterInitialization()方法负责。

现在考虑这个场景:ServiceA需要被AOP代理(比如加了@Transactional注解),同时ServiceA和ServiceB相互依赖。

如果使用二级缓存:

  1. 实例化ServiceA → 放入二级缓存(此时是原始对象)
  2. 属性注入ServiceB → 创建ServiceB
  3. ServiceB需要注入ServiceA → 从二级缓存获取ServiceA(原始对象)
  4. ServiceB创建完成 → 回到ServiceA的初始化
  5. 初始化ServiceA → 创建代理对象 → 放入一级缓存

问题出现了:ServiceB持有的是ServiceA的原始对象引用,而容器中存储的是代理对象。这两个不是同一个对象!

这会导致严重的问题:事务注解、日志切面等功能全部失效,因为ServiceB调用的是原始对象而非代理对象。

三级缓存:延迟代理创建的优雅解法

Spring的解决方案是引入第三级缓存:

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

三级缓存存储的不是对象本身,而是一个ObjectFactory——一个函数式接口,其getObject()方法可以在需要时才执行代理创建逻辑。

// 在doCreateBean方法中,实例化后立即注册到三级缓存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

getEarlyBeanReference()方法会调用所有SmartInstantiationAwareBeanPostProcessor的实现,其中AbstractAutoProxyCreator(AOP代理创建的核心类)会在此处判断是否需要创建代理对象。

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

三级缓存的精妙之处在于延迟执行:只有在真正发生循环依赖时,才会调用ObjectFactory.getObject()创建代理对象。如果没有循环依赖,这个工厂函数永远不会被执行,代理对象会在正常的初始化后阶段创建。

这解释了为什么不能只用二级缓存:如果提前在实例化阶段就创建代理对象并放入二级缓存,那么所有Bean(无论是否有循环依赖)都会在实例化阶段就创建代理,这违背了Spring的设计初衷——代理创建应该在Bean完全初始化后进行。

完整的循环依赖解决流程

让我们用一个具体例子来梳理完整流程。ServiceA和ServiceB相互依赖,且ServiceA需要被AOP代理:

sequenceDiagram
    participant Container as Spring容器
    participant Cache as 三级缓存系统
    participant A as ServiceA
    participant B as ServiceB

    Container->>Cache: 创建ServiceA
    Cache->>A: 实例化ServiceA(原始对象)
    Cache->>Cache: 注册ObjectFactory到三级缓存
    Note over Cache: singletonFactories.put("serviceA", factory)
    
    Container->>Cache: 属性注入ServiceB
    Container->>Cache: 创建ServiceB
    Cache->>B: 实例化ServiceB
    Cache->>Cache: 注册ObjectFactory到三级缓存
    
    Container->>Cache: ServiceB需要注入ServiceA
    Cache->>Cache: getSingleton("serviceA")
    Note over Cache: 一级缓存未命中
    Note over Cache: 二级缓存未命中
    Note over Cache: 三级缓存命中ObjectFactory
    Cache->>Cache: 调用getObject()创建代理
    Note over Cache: 移动到二级缓存
    Cache-->>B: 返回ServiceA的代理对象
    
    Cache->>Cache: ServiceB创建完成
    Cache->>Cache: ServiceA继续初始化
    Note over Cache: 从二级缓存获取已创建的代理
    Cache->>Cache: 放入一级缓存

核心代码在DefaultSingletonBeanRegistry.getSingleton()方法中:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 先从一级缓存获取
    Object singletonObject = this.singletonObjects.get(beanName);
    
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 2. 一级缓存没有,从二级缓存获取
        singletonObject = this.earlySingletonObjects.get(beanName);
        
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // 双重检查
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 3. 从三级缓存获取ObjectFactory
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            // 执行工厂方法,可能创建代理对象
                            singletonObject = singletonFactory.getObject();
                            // 移入二级缓存
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            // 从三级缓存移除
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

Spring无法解决的循环依赖场景

三级缓存机制很强大,但并非万能。以下场景Spring无法自动解决循环依赖:

构造器注入

构造器注入要求在对象创建时就传入所有依赖。当两个Bean都使用构造器注入相互依赖时:

@Component
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private final ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

创建ServiceA时,构造函数需要ServiceB,但ServiceB还未创建;创建ServiceB时,构造函数又需要ServiceA——这是一个无法打破的死循环。对象在构造函数执行完成前根本不存在,自然无法提前暴露引用。

解决方案是使用@Lazy注解:

public ServiceA(@Lazy ServiceB serviceB) {
    this.serviceB = serviceB;
}

@Lazy会告诉Spring注入一个代理对象,只有在真正调用方法时才会创建实际对象。

原型作用域

三级缓存机制只适用于单例(Singleton)作用域的Bean。对于原型(Prototype)作用域的Bean,Spring不缓存实例,每次请求都会创建新对象,循环依赖自然无法解决。

@Async注解

这是一个容易被忽视的陷阱。@Async注解使用AsyncAnnotationBeanPostProcessor创建代理,但这个后置处理器没有实现SmartInstantiationAwareBeanPostProcessor接口

这意味着当发生循环依赖时,getEarlyBeanReference()方法不会被调用,@Async的代理对象无法在早期暴露阶段创建。最终结果是:早期暴露的对象和最终创建的对象不一致,Spring会抛出BeanCurrentlyInCreationException

解决方案包括:

  1. 在循环依赖的字段上添加@Lazy注解
  2. 将异步方法提取到独立的Bean中,避免循环依赖

Spring Boot 2.6:默认禁用循环依赖

2021年12月发布的Spring Boot 2.6做出了一个重要决定:默认禁止循环引用

Spring Boot 2.6的发布说明中明确写道:

Circular references between beans are now prohibited by default. If your application fails to start due to a BeanCurrentlyInCreationException you are strongly encouraged to update your configuration to break the dependency cycle.

这一变化反映了Spring团队的态度:循环依赖往往是设计问题的信号。它通常意味着职责划分不清晰,违反了单一职责原则。

如果确实需要启用循环依赖支持,可以设置:

spring.main.allow-circular-references=true

但这只是一个过渡方案,最佳实践是重构代码结构,消除循环依赖。

设计启示:权衡的艺术

Spring的三级缓存设计展示了软件工程中权衡的艺术:

为什么不用一级缓存? 需要全局锁,性能太差。

为什么不用二级缓存? 无法优雅处理AOP代理,会导致提前创建代理对象。

为什么三级缓存可以? 延迟执行的工厂模式,只在真正需要时才创建代理,既解决了循环依赖,又保持了代理创建的正常时机。

这套机制的本质是:用空间换时间,用延迟换简单。三级缓存看似复杂,实则是多种约束条件下最优的工程解法。


参考文献

  1. Spring Framework Documentation - The IoC Container: https://docs.spring.io/spring-framework/reference/core/beans/
  2. Spring Boot 2.6 Release Notes: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes
  3. Baeldung - Circular Dependencies in Spring: https://www.baeldung.com/circular-dependencies-in-spring
  4. Spring源码分析系列-循环依赖和三级缓存: https://zhuanlan.zhihu.com/p/375308988
  5. DefaultSingletonBeanRegistry Source Code: https://docs.spring.io/spring-framework/docs/3.2.0.M2_to_3.2.0.RC1/Spring%20Framework%203.2.0.RC1/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.html