Spring 循环依赖不止三级缓存,Boot 2.6 起这个开关默认已经关了

➡️ 程序员曜灵 · 后端面试追问链 - 欢迎认识我

作者程序员曜灵,绿泡泡「我要拿offer」和小红书同名。

主业在一家大型央企做后端开发,Java 方向,参与过公司内部招聘面试。

这里在拆高频面试题的追问链,一题三层,每层给及格线答案和大多数人挂在哪。

工作日每天一篇,评论区点最高频的问题决定下一篇写什么。

AbstractAutowireCapableBeanFactory 第 131 行写着下面这句,上面一行的注释是 Whether to automatically try to resolve circular references between beans。

java 复制代码
private boolean allowCircularReferences = true;

这是 Spring 框架侧的字段,它一直是 true。所以「Spring 能解循环依赖」这句话在框架层面成立,三级缓存那套解释也没错。

问题在下一层。Spring Boot 从 2.6 起把这个动作的默认值翻了个方向,配置项 spring.main.allow-circular-references 的默认值是 false,2.6.0 那份 reference 的属性表里就是这么列的。我昨天在写另一篇稿子时取过这张表和配套的 Release Notes,两段原文都留下了。

复制代码
Circular References Prohibited by Default

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. If you are unable to do so, circular references can be allowed again
by setting spring.main.allow-circular-references to true, or using the new
setter methods on SpringApplication and SpringApplicationBuilder. This will
restore 2.5's behaviour and automatically attempt to break the dependency
cycle.

这两个默认值放在一起,才是面试里那句「Spring 怎么解决循环依赖」真正需要的上下文。框架给了能力,Boot 默认没开。我在评审别人的代码时见过不止一次,同一个类在老项目里跑得好好的,搬到 Boot 2.7、3.x 的工程就起不来,报 BeanCurrentlyInCreationException,然后有人第一反应是去查三级缓存哪一级没生效。方向从一开始就偏了。

提前暴露这一步挂着三个与条件

Boot 是在哪儿把 false 传下去的,我这次没取到源码位置,api.github.com 上那几个猜测路径都是 404,代码搜索接口要鉴权。所以下面只讲我逐字读到的那一行,它决定一个 bean 到底能不能被提前暴露。

java 复制代码
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    ...
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

三个条件按与运算串着,断在哪个上,症状就不一样。

mbd.isSingleton() 管作用域。bean 是 prototype 就走到这里为止,循环依赖解不了。原因不神秘,prototype 每次 getBean 都要新建一个实例,提前暴露给它没有意义,下一级缓存本来就不是为它准备的。一个 singleton 注进一个 prototype,同样落空。

this.allowCircularReferences 管开关。Boot 2.6 以上没改配置,这里就是 false,整段提前暴露根本不执行,两个互相引用的单例直接撞异常。

isSingletonCurrentlyInCreation(beanName) 管时机。只有这个 bean 此刻正在创建中,才存在「先给一个半成品」的必要。正常拿到已完成实例的场景用不着这条路。

我把这段拆开来记,是因为它能把「哪些情况解不了」从一个背出来的清单变成推出来的结论。三种失效各对应一类现场,不用记第四个。

启动失败那行原文在说什么

真正容易被当成「三级缓存失灵」的,是下面这个异常。AbstractAutowireCapableBeanFactory 里的构造文案我逐字抄下来。

复制代码
Bean with name 'xxx' has been injected into other beans [yyy] in its raw
version as part of a circular reference, but has eventually been wrapped.
This means that said other beans do not use the final version of the bean.

它说的是这么回事。A 在创建过程中把未完成的自己交给了 B,交出去那一下是裸对象。等 A 走完初始化,后置处理器给它套了层代理,最终放进单例池的是包装后的版本。B 手上那个裸对象和容器里的最终版本成了两个东西。Spring 的做法是不放过这个不一致,直接抛异常。

旁边第 137 行有个字段管这件事的另一半。

java 复制代码
private boolean allowRawInjectionDespiteWrapping = false;

默认 false,意思是「裸对象注入后又被包装」这种情况不允许。把它设成 true 能让启动过去,代价是 B 持有的实例没有切面,@Transactional 之类的东西在那条调用链上静默失效。这个开关我只在生产事故里见过用途,平时它不该出现在配置里。

三级缓存具体是哪三张 map

DefaultSingletonBeanRegistry 第 86、89、95 行,声明原文连着注释。

java 复制代码
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Creation-time registry of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);

private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

第三级存放的是 ObjectFactory,一个 lambda,内容是 getEarlyBeanReference(beanName, mbd, bean)。这一句直接回答「为什么二级不够」。

如果只有两级,提前暴露时就得当场决定给不给代理。给代理,就把初始化时机提前到属性填充之前,BeanPostProcessor 该干的活被硬拽出来做了一遍;不给代理,后面发现这个 bean 需要被 AOP 包装,B 手上就成了错的那份。放进第三级当工厂,谁真来取才现场决定,不取就按正常顺序走完初始化。多出来的一级是为这个「等一等」存在的。

解法按代价选,不看流行度

改字段注入或 setter 注入能让它过。这条路依赖的正是提前暴露,它治的是「构造期就互相要」,对 @Async 那类代理时机特殊的场景没用,该报还是报。

@Lazy 在注入点拿的是一个代理,真调用时才去找目标实例,启动期的环被打断了。代价是循环本身还在那儿,只是延后引爆,而且多一层代理转发。我在评审里一般让它当临时方案,挂个待办,不当结论。

ObjectProvider 是 org.springframework.beans.factory 包下的接口,javadoc 标着 @since 4.3,作者 Juergen Hoeller。它把「现在就要」改成「用到时再取」,注入点本身不触发创建,比 @Lazy 少一层代理语义,更适合只有一条调用方向的情况。

真要靠得住的动作还是拆。两个 bean 互相引用,通常是其中一个把两件事揽在一起,抽出第三个 bean 或者用事件把它解开,依赖方向就单向了。官方那句是 update your configuration to break the dependency cycle,上面这些取舍是我自己的判断。

配置项、字段和它们现在的默认值

名称 默认值 版本 出处
spring.main.allow-circular-references false Spring Boot 2.6 起 2.6.0 reference 属性表(2026-10-13 核)
allowCircularReferences(框架字段) true Spring 框架侧一直如此 AbstractAutowireCapableBeanFactory 第 131 行
allowRawInjectionDespiteWrapping false 同上 同文件第 138 行
earlySingletonExposure 由三个与条件算出 同上 同文件第 588 行
singletonObjects ConcurrentHashMap 初容 256 同上 DefaultSingletonBeanRegistry 第 86 行
singletonFactories ConcurrentHashMap 初容 16 同上 同文件第 89 行
earlySingletonObjects ConcurrentHashMap 初容 16 同上 同文件第 95 行
ObjectProvider 接口,无默认值 @since 4.3 ObjectProvider.java javadoc
循环依赖默认禁止这一变更 需显式设 true 才恢复旧行为 Boot 2.5 升 2.6 2.6 Release Notes 原文(2026-10-13 核)

这道题我会怎么答

第一层问「Spring 怎么解决循环依赖」。及格线是把三级缓存的名字背出来。这一层守不住追问,因为背下来的是机制的名字,缺的是它成立的条件。

第二层问「那什么时候解决不了」。大多数人到这里开始背清单,构造器注入、prototype、@Bean 方法互相调用,能背四五条,但说不出为什么。这里我给的是那行三条件,从与运算里推,推出来的每一条都带一个具体后果。

第三层问「Boot 2.6 之后有什么变化」。这是最容易翻车的一层。有人答 allowCircularReferences 默认是 true,这句在框架字段上成立,在 Boot 项目的实际行为上已经反了。新项目的正确回答是默认禁止,启动报 BeanCurrentlyInCreationException,要么改依赖结构,要么显式设 spring.main.allow-circular-references=true 恢复 2.5 的行为,而后者官方那句是 strongly encouraged to update your configuration,意思是不建议你把这条路当长期方案。

一处交代,框架侧那几行是我今天从 spring-projects 官方仓库逐字取的,Boot 侧两处是 2026-10-13 取的原文,日期都在出处里标了。Boot 把开关置为 false 的确切代码位置没取到,文中按未核到处理。@Async 场景的具体异常栈我这次没跑复现,只按三条件推它会落在哪,这句话是推断。

出处

allowCircularReferences、allowRawInjectionDespiteWrapping、earlySingletonExposure 那段与运算、BeanCurrentlyInCreationException 的文案,取 spring-projects/spring-framework 仓库 master 分支 AbstractAutowireCapableBeanFactory.java,三张 map 的声明与注释取同仓库 DefaultSingletonBeanRegistry.java 第 86、89、95 行,@since 4.3 取 ObjectProvider.java 的类注释。三处均于 2026-10-14 通过 GitHub 的仓库接口取到原文。spring.main.allow-circular-references 的默认值取 Spring Boot 2.6.0 reference 的属性表,Circular References Prohibited by Default 一段取 spring-boot 项目 wiki 的 2.6 Release Notes,两处于 2026-10-13 取到。头部文章的覆盖情况来自 2026-10-13 对五篇高阅读文章的逐篇正文核对,配置项的点号形式在它们正文里出现 0 次。解法取舍一段是作者判断。

最后

如果觉得这篇有用,可以点赞+收藏+关注 支持一下,程序员曜灵的主页还有很多按追问链拆的面试题,欢迎前去点评。

哪里讲得不准,评论区指出,我改在这篇的更新里。欢迎学习交流、面试问题求助、共同进步。

绿泡泡【我要拿offer】,回复 Spring 拿那份按模块整理的高频面试题 PDF。想看每日面经和 offer 案例的,小红书同名号程序员曜灵。

相关推荐
SL_staff1 小时前
3分钟上线新规则:业务人员拖拽完成合同超期自动暂停服务
java·mysql·全栈
Android小码家1 小时前
Vehicle 实战 自定义属性+测试demo
java·c++·framework·vhal·vehicle
范同学~1 小时前
固定表头+第一列+最后一列
java·服务器·前端
随风爱学习2 小时前
JAVA学习之异常
java
m0_639906942 小时前
HashMap:哈希扰动、扩容源码、红黑树转换、并发问题
java·算法·哈希算法
Roll圈圈2 小时前
长链接太丑影响转化率?这个短链工具让你的推广链接更专业
java
神魔皆以血嗜2 小时前
Java 存储金额
java·后端
萧瑟余晖2 小时前
Spring Cloud微服务总览与注册发现详解
spring·spring cloud·微服务
SL_staff3 小时前
周报越厚问题越拖?用结构化执行机制让周报回归决策价值
java·spring cloud·开源