Spring 循环依赖与三级缓存

举报
yd_94907100 发表于 2026/10/31 21:59:50 2026/10/31
【摘要】 拆解三级缓存的存取时机,说明哪种循环依赖能解、哪种不能。

循环依赖是面试高频题,也是线上启动失败的常见原因。很多人能背出"三级缓存"三个字,却说不清为什么必须是三级而不是两级,也说不清为什么构造器注入的循环依赖解不了。本文从可复现的报错出发,沿着 DefaultSingletonBeanRegistry 的源码,把这件事一次讲透。适合已经能熟练使用 Spring、想理解容器内部机制的后端开发阅读。

一、问题复现

两个 Service 互相持有对方:

@Service
public class AService {
    @Autowired
    private BService bService;
}

@Service
public class BService {
    @Autowired
    private AService aService;
}

这段代码在默认配置下可以正常启动。把注入方式换成构造器:

@Service
public class AService {
    private final BService bService;
    public AService(BService bService) { this.bService = bService; }
}

启动报错:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  aService defined in file [.../AService.class]
↑     ↓
|  bService defined in file [.../BService.class]
└─────┘

Spring Boot 2.6 之后默认禁止循环引用,字段注入也会报同样的错,需要显式开启:

spring:
  main:
    allow-circular-references: true

Spring Boot 3.x 沿用这一默认值。也就是说,三级缓存仍然存在,但默认不再被用来解业务 Bean 的循环依赖,这是很多人版本升级后启动失败的原因。

二、为什么构造器注入解不了

Bean 的创建顺序是:实例化 → 属性填充 → 初始化。字段注入发生在属性填充阶段,此时对象已经在堆上,可以先暴露引用再赋值;构造器注入要求依赖在实例化之前就准备好,形成了不可打破的先后约束。

createBeanInstance 调用构造器时,AService 需要 BService 实例,BService 的构造又需要 AService 实例,两者都没有完成实例化,容器无对象可暴露,只能抛 BeanCurrentlyInCreationException。这不是缓存层数的问题,而是时序上根本无解。

三、三级缓存的数据结构

DefaultSingletonBeanRegistry 中定义了三个 Map:

/** 一级:完整的单例 Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 三级:ObjectFactory,用于生成早期引用(可能是代理) */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** 二级:提前暴露的早期 Bean,尚未完成属性填充与初始化 */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

此外还有两个辅助集合:

/** 正在创建中的 Bean 名称 */
private final Set<String> singletonsCurrentlyInCreation =
        Collections.newSetFromMap(new ConcurrentHashMap<>(16));

/** 已经创建完成的 Bean 名称,用于判断是否需要提前暴露 */
private final Set<String> registeredSingletons = new LinkedHashSet<>(256);

getSingleton(beanName) 的查找顺序是严格的一级 → 二级 → 三级:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        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) {
                        ObjectFactory<?> factory = this.singletonFactories.get(beanName);
                        if (factory != null) {
                            singletonObject = factory.getObject();
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

注意三级取到值后会升级到二级并删除三级,所以一个 Bean 的早期引用只会被生成一次。

四、A 与 B 的创建时序

以字段注入为例,完整过程如下:

  1. getBean("aService") → doCreateBean → createBeanInstance 反射创建 A 的原始对象。
  2. 判断允许提前暴露(单例、正在创建、且非合成 Bean),执行 addSingletonFactory,把 () -> getEarlyBeanReference("aService", mbd, a) 放入三级缓存。
  3. populateBean 发现需要 BService,调用 getBean("bService")。
  4. B 同样实例化后把工厂放入三级缓存,populateBean 时发现需要 AService。
  5. getSingleton("aService", true) 在三级缓存命中工厂,调用 getEarlyBeanReference 得到 A 的早期引用,放入二级缓存,删除三级缓存。
  6. B 拿到 A 的早期引用完成注入,走完初始化,放入一级缓存。
  7. A 继续完成 populateBean(此时拿到的是完整的 B)、初始化,放入一级缓存,清理二级缓存。

核心代码在 getEarlyBeanReference:

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;
}

五、为什么需要第三级

如果只有两级(一级存完整对象、二级存早期对象),在第 2 步就必须立刻决定"要不要为 A 生成代理",但此时 A 还没有完成属性填充,AbstractAutoProxyCreator 判断是否需要代理依赖目标对象的类型和注解,贸然生成可能得到错误结果。

三级缓存存的是 ObjectFactory,把"是否生成代理"这个动作延迟到真正被循环引用时才执行。没有被循环引用时,工厂永远不会被调用,A 只在初始化后由 AbstractAutoProxyCreator#postProcessAfterInitialization 生成一次代理。这是"按需"与"提前"的差别。

AbstractAutoProxyCreator 用一个 earlyProxyReferences 记录已经生成过早期代理的 Bean,避免初始化后重复代理:

@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
}

@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

六、能解与不能解的情形

| 场景 | 能否解决 | 原因 |

| --- | --- | --- |

| 单例 + 字段/setter 注入 | 能 | 实例化后即可暴露早期引用 |

| 单例 + 构造器注入 | 不能 | 实例化前就需要依赖 |

| 原型 Bean 互相依赖 | 不能 | 原型不缓存,也不做提前暴露 |

| @Async 代理的循环依赖 | 不能 | 需在初始化后由 AsyncAnnotationBeanPostProcessor 生成代理,早期引用拿不到 |

| @Transactional 循环依赖 | 有条件能 | AbstractAutoProxyCreator 支持 getEarlyBeanReference |

| @Scope("prototype") 参与循环 | 不能 | 同上,无缓存可复用 |

@Async 之所以失败,是因为它依赖的 AsyncAnnotationBeanPostProcessor 只实现了 BeanPostProcessor,没有实现 SmartInstantiationAwareBeanPostProcessor,无法提供早期引用。

七、正确写法

7.1 重构,消除环

绝大多数循环依赖是职责划分不清的信号。A 依赖 B、B 依赖 A,通常意味着存在第三个被两边共享的职责,抽出来即可:

@Service
public class OrderQueryService {   // 从 OrderService 抽出的只读能力
    private final OrderRepository repository;
    public OrderQueryService(OrderRepository repository) { this.repository = repository; }
}

@Service
public class OrderService {
    private final OrderQueryService queryService;
    private final PayService payService;   // 单向依赖
    public OrderService(OrderQueryService queryService, PayService payService) {
        this.queryService = queryService;
        this.payService = payService;
    }
}

7.2 用 @Lazy 打破时序

无法立刻重构时,在其中一个注入点加 @Lazy,容器会注入一个延迟解析的代理:

@Service
public class AService {
    private final BService bService;
    public AService(@Lazy BService bService) { this.bService = bService; }
}

@Lazy 注入的是 CGLIB 代理,首次调用方法时才真正 getBean。代价是每次调用多一层代理开销,且类型判断、instanceof 会失真。

7.3 用 ApplicationContext 延迟获取

@Service
public class AService implements ApplicationContextAware {
    private ApplicationContext ctx;
    public void doSomething() {
        ctx.getBean(BService.class).handle();
    }
    @Override
    public void setApplicationContext(ApplicationContext ctx) { this.ctx = ctx; }
}

这种方式把编译期依赖变成运行期查找,牺牲了可测试性,只适合兜底。

八、常见坑 / 最佳实践

  1. 升级 Spring Boot 2.6+ 后启动失败就打开 allow-circular-references。这会把设计问题掩盖成配置问题。正确做法是先看报错里打印的依赖环,评估能否抽类重构。
  2. 以为加了 @Lazy 就万事大吉。@Lazy 生成的是代理,如果目标类是 final 或方法 final,CGLIB 无法代理,会退化为 JDK 代理并抛类型转换异常。
  3. 在 @PostConstruct 中调用循环依赖方的方法。此时对方可能只是早期引用,属性尚未填充,容易出现 NullPointerException。
  4. @Async 与循环依赖同时出现。这类组合无法靠三级缓存解决,应把异步方法抽到独立 Bean,让调用方单向依赖它。
  5. 原型 Bean 上尝试循环依赖。容器不缓存原型,任何"提前暴露"都无效,必须在设计上断开。

九、小结

  • 三级缓存分别是 singletonObjects、earlySingletonObjects、singletonFactories,查找顺序一级 → 二级 → 三级。
  • 第三级存 ObjectFactory,目的是把代理生成延迟到真正被引用时,保证普通 Bean 只代理一次。
  • 构造器注入、原型 Bean、@Async 代理的循环依赖无法靠三级缓存解决。
  • Spring Boot 2.6 起默认禁止循环引用,遇到报错优先重构而非改配置。
  • 循环依赖多数是职责划分问题的信号,@Lazy 只是缓解手段。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。