一句结论(面试开场可背)
Spring 对 singleton bean 支持循环依赖,靠的是三层缓存(三级缓存)+ 早期引用(Early Reference)机制,在创建 bean 的过程中提前暴露一个“可用引用”(可能是原生对象或代理)给其它正在创建的 bean 使用,从而打破环路。
但 构造器注入(constructor injection) 的循环依赖 无法被自动解决,除非使用 @Lazy
、ObjectFactory/Provider
、或改写为 setter/字段注入。
核心概念:三层缓存(DefaultSingletonBeanRegistry)
Spring 在 DefaultSingletonBeanRegistry
中维护三种缓存(针对 singleton):
-
singletonObjects(一级缓存) — 已完全创建并初始化好的单例实例(完整 bean)。
-
earlySingletonObjects(二级缓存) — 早期曝光的单例实例(通常是实例化后、但还没完成依赖注入或初始化的对象),用于解决简单循环依赖。
-
singletonFactories(三级缓存) — 存放
ObjectFactory
(工厂方法),当需要早期引用时会调用这个工厂返回一个“早期引用”。这个工厂通常会返回经过BeanPostProcessor
(如 AOP 的getEarlyBeanReference
)处理后的代理对象,以支持代理情形下的循环依赖。
缓存关系(简化):
创建流程要点(关键方法与时点,基于 AbstractAutowireCapableBeanFactory.doCreateBean)
下面按时间序列说明大致流程(已简化):
-
实例化(instantiateBean / createBeanInstance)
-
调用构造器(或工厂方法)创建原始对象(尚未注入属性)。此时 BeanName 标记为“正在创建中”。
-
-
暴露早期对象工厂(重要)
-
如果允许循环依赖且是 singleton,Spring 会在属性注入前把一个
singletonFactory
注册到singletonFactories
: -
也就是说,在 populateBean(属性注入)之前就把工厂放进三级缓存。这是实现早期曝光的关键时机。
-
-
依赖注入(populateBean)
-
Spring 为 bean 注入属性(autowire byName/byType、@Autowired 等)。当注入另一个还在创建的 bean 时,消费者会调用
getSingleton
获取该 bean 的引用:-
getSingleton
先查一级缓存(已完成),没有则查二级缓存(earlySingletonObjects),没有就查三级(singletonFactories),若存在 factory 则调用 factory.getObject() 生成早期引用并放入二级缓存,之后移除 factory。
-
-
因此,即便依赖的 bean 尚未完成初始化,也可以拿到早期引用(通常是实例或代理),从而断开环路。
-
-
初始化(initializeBean)
-
进行
BeanPostProcessor
前置/后置处理、@PostConstruct
、afterPropertiesSet
、AOP 代理创建等。 -
如果 AOP 需要生成代理,
getEarlyBeanReference
(由SmartInstantiationAwareBeanPostProcessor
)会在早期引用阶段返回代理对象,这样注入方拿到的就是代理,而不是裸对象。
-
-
注册为完全可用(addSingleton)
-
初始化完成后,把最终实例放入一级缓存(singletonObjects),并清理二三级缓存中的相关项。
-
关键类 / 方法 / 作用(面试要点)
-
DefaultSingletonBeanRegistry
:维护三缓存,并提供addSingletonFactory
、getSingleton
、registerSingleton
等方法。 -
AbstractAutowireCapableBeanFactory#doCreateBean(...)
:bean 创建核心流程;在 populateBean 前调用addSingletonFactory
暴露早期工厂(如果允许循环依赖)。 -
getEarlyBeanReference(...)
:调用BeanPostProcessor
(如 AOP 的getEarlyBeanReference
)以返回可能的代理,保证注入方拿到合适的引用。 -
SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(...)
:AOP 使用此入口在早期生成代理,典型实现:AopInfrastructureBean
/ProxyFactory
。
(面试举例写出以上类名会很加分)
为什么构造器注入不能自动解决?
-
构造器注入需要在对象实例化(即调用构造函数)时就传入依赖对象;也就是说,两个相互通过构造器注入的 bean 都必须在对方构造期间得到对方的引用——这是无法通过“实例化后再注入早期引用”的机制解决的(因为早期引用是在实例化后、在属性注入前暴露的)。
-
因此 Spring 不支持构造器循环依赖(会抛出
BeanCurrentlyInCreationException
或类似错误),除非采用以下变通办法:-
把其中一个依赖声明为
@Lazy
(延迟获取),这样 Spring 在构造时不会立刻要求该依赖; -
或改用
ObjectFactory<T>
/Provider<T>
或ApplicationContext.getBean()
在运行时延迟获取; -
或改成 setter/字段注入(让 Spring 能暴露早期引用并在后续注入)。
-
代理(AOP)与早期引用的复杂性
-
如果某个 bean 会被 AOP 代理(比如有事务、@Transactional、或其他切面),注入方需要的是代理对象而非原始对象,否者代理相关的横切逻辑将失效。
-
因此 Spring 在三级缓存中放入的 factory 并非简单返回原始 bean,而是调用
getEarlyBeanReference
,该方法允许SmartInstantiationAwareBeanPostProcessor
(如AbstractAutoProxyCreator
)在早期暴露代理对象。这保证了注入方拿到的是代理(如果需要),从而保证 AOP 行为在循环依赖场景下仍然正确。 -
注意:如果不使用 early-proxy(即 factory 仅返回裸对象),注入方可能在后期会被替换为代理(初始化后),这会造成注入方持有的不再是最终代理,进而出现 AOP 失效或不一致问题。Spring 的 early proxy 机制就是为了解决这种问题。
哪些场景 Spring 无法或不建议处理循环依赖
-
构造器注入循环依赖:不支持(unless
@Lazy
或 Provider)。 -
prototype 作用域(多例):Spring 默认不做循环引用处理,若发现循环会抛异常;因为 prototype 的生命周期复杂,容器不会缓存早期引用。
-
final 字段 / 不可变对象:需要在构造时传入的依赖无法通过 setter 注入解决。
-
复杂 AOP 场景或自定义 BeanPostProcessor 未正确实现 getEarlyBeanReference:可能导致注入方拿到不正确的早期引用或代理不一致。
如何在工程中避免或稳妥处理循环依赖(最佳实践)
-
优先用构造器注入来表达必需依赖(更清晰、便于测试);但若构造器注入导致循环,说明设计上耦合度过高,应重构(抽取接口/合并组件/引入中间层)。
-
对可选或延迟的依赖使用
@Lazy
或ObjectProvider<T>
/javax.inject.Provider
/ObjectFactory<T>
。这能延迟获取并破环循环。 -
用 setter 注入或字段注入(注意测试与可见性),让 Spring 能早期暴露引用。
-
重构以减少互相依赖:抽取第三个共同依赖(Service C),把部分逻辑移到 C。
-
避免把事务边界放在循环依赖的 bean 上,或谨慎处理 AOP 与代理问题。
-
使用单例设计与职责单一原则:高内聚、低耦合,减少循环依赖的出现概率。
面试常见问法与示例回答(摘要)
-
问:Spring 是怎么解决循环依赖的?
答(要点):-
只对 singleton 生效(prototype 不支持)。
-
利用
DefaultSingletonBeanRegistry
的三级缓存(singletonObjects / earlySingletonObjects / singletonFactories)在属性注入前提前暴露早期引用(early reference)。 -
在
doCreateBean
的 populate 阶段之前用addSingletonFactory
注册工厂,其他 bean 请求时会通过 factory 得到早期引用。 -
对于代理(AOP)会通过
getEarlyBeanReference
返回代理,保证注入方拿到的是代理而不是裸对象。 -
构造器注入循环依赖不能解决,应改为延迟注入或重构。
-
-
给出细节(提到类名/方法名/三缓存)会显著加分。
补充:示例说明(简短伪代码)
假设 A 依赖 B,B 依赖 A,且都是 singleton 且用字段/setter 注入:
-
创建 A:实例化 A(对象 created)→
addSingletonFactory("A", () -> getEarlyBeanReference(A))
→ populate A(需要 B) -
在 populate A 时需要 B,触发创建 B:实例化 B → 因为
singletonFactory("A")
存在,B 注入 A 时会通过getSingleton("A")
得到 earlyReference(factory 创建可能是 proxy 或 raw) → B 注入 A 的引用继续完成 → B 初始化完成并注册为 singletonObjects。 -
返回到 A 的 populate,A 注入 B(已完成),A 初始化完成后把 A 放到 singletonObjects(并清理 early caches)。流程顺利,循环被打破。