惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google Online Security Blog
Google Online Security Blog
博客园_首页
Martin Fowler
Martin Fowler
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
博客园 - 叶小钗
Jina AI
Jina AI
罗磊的独立博客
Simon Willison's Weblog
Simon Willison's Weblog
Scott Helme
Scott Helme
D
DataBreaches.Net
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
Know Your Adversary
Know Your Adversary
博客园 - Franky
AWS News Blog
AWS News Blog
S
Schneier on Security
K
Kaspersky official blog
I
Intezer
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
L
LINUX DO - 热门话题
GbyAI
GbyAI
月光博客
月光博客
C
Cisco Blogs
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
G
GRAHAM CLULEY
P
Privacy International News Feed
P
Privacy & Cybersecurity Law Blog
Hugging Face - Blog
Hugging Face - Blog
P
Proofpoint News Feed
T
Tor Project blog
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
T
Threat Research - Cisco Blogs
IT之家
IT之家
H
Hackread – Cybersecurity News, Data Breaches, AI and More
H
Help Net Security
S
Security Affairs
V
Visual Studio Blog
C
CERT Recently Published Vulnerability Notes
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
S
SegmentFault 最新的问题
MongoDB | Blog
MongoDB | Blog
T
Troy Hunt's Blog

博客园 - 郭慕荣

彻底搞懂 Codex Skill 与插件:区别、试用、核心作用、项目落地全指南(程序员实战向) Codex Skill 和插件到底有什么区别?程序员如何在项目中落地使用 claudecode 配置防止配置文件泄露总结 Claude code分析外部文件总结 Claude code三种模式详解 hasCompletedOnboarding详解 Claudecode 有哪些常用的命令 claude.md文件详解 claude code 的setting文件详解 Docker部署zookeeper总结 Mac中git ssh的配置(GitLab) Java 类加载机制 面试题(一) 如何监控和调优JVM性能? jvm常用的参数有哪些? 怎么配置? hashmap 和currenthashmap 的原理?详解一下 aop在项目中使用的场景?怎么使用? Java dubbo spring springboot中的spi机制 spring中常见的两种代理模式 Redis的zset 面试汇总 drools 规则引擎在线化配置 springcloud中常用的注解详解 springcloud中网关gateway总结 MySQL 死锁 怎么处理? 在写left join的时候 是大表在左侧 还是小表在左侧(二) 在写left join的时候 是大表在左侧 还是小表在左侧(一) nacos客户端(接口调用者)如何感知被调用服务下线? (二) nacos客户端(接口调用者)如何感知被调用服务下线?(一) 在MySQL中 redolog undolog binlog 写入的场景,顺序
spring是怎么解决循环依赖的?
郭慕荣 · 2025-10-21 · via 博客园 - 郭慕荣

一句结论(面试开场可背)

Spring 对 singleton bean 支持循环依赖,靠的是三层缓存(三级缓存)+ 早期引用(Early Reference)机制,在创建 bean 的过程中提前暴露一个“可用引用”(可能是原生对象或代理)给其它正在创建的 bean 使用,从而打破环路。
构造器注入(constructor injection) 的循环依赖 无法被自动解决,除非使用 @LazyObjectFactory/Provider、或改写为 setter/字段注入。


核心概念:三层缓存(DefaultSingletonBeanRegistry)

Spring 在 DefaultSingletonBeanRegistry 中维护三种缓存(针对 singleton):

  1. singletonObjects(一级缓存) — 已完全创建并初始化好的单例实例(完整 bean)。

  2. earlySingletonObjects(二级缓存) — 早期曝光的单例实例(通常是实例化后、但还没完成依赖注入或初始化的对象),用于解决简单循环依赖。

  3. singletonFactories(三级缓存) — 存放 ObjectFactory(工厂方法),当需要早期引用时会调用这个工厂返回一个“早期引用”。这个工厂通常会返回经过 BeanPostProcessor(如 AOP 的 getEarlyBeanReference)处理后的代理对象,以支持代理情形下的循环依赖。

缓存关系(简化):


创建流程要点(关键方法与时点,基于 AbstractAutowireCapableBeanFactory.doCreateBean)

下面按时间序列说明大致流程(已简化):

  1. 实例化(instantiateBean / createBeanInstance)

    • 调用构造器(或工厂方法)创建原始对象(尚未注入属性)。此时 BeanName 标记为“正在创建中”。

  2. 暴露早期对象工厂(重要)

    • 如果允许循环依赖且是 singleton,Spring 会在属性注入前把一个 singletonFactory 注册到 singletonFactories

    • 也就是说,在 populateBean(属性注入)之前就把工厂放进三级缓存。这是实现早期曝光的关键时机。

  3. 依赖注入(populateBean)

    • Spring 为 bean 注入属性(autowire byName/byType、@Autowired 等)。当注入另一个还在创建的 bean 时,消费者会调用 getSingleton 获取该 bean 的引用:

      • getSingleton 先查一级缓存(已完成),没有则查二级缓存(earlySingletonObjects),没有就查三级(singletonFactories),若存在 factory 则调用 factory.getObject() 生成早期引用并放入二级缓存,之后移除 factory。

    • 因此,即便依赖的 bean 尚未完成初始化,也可以拿到早期引用(通常是实例或代理),从而断开环路。

  4. 初始化(initializeBean)

    • 进行 BeanPostProcessor 前置/后置处理、@PostConstructafterPropertiesSet、AOP 代理创建等。

    • 如果 AOP 需要生成代理,getEarlyBeanReference(由 SmartInstantiationAwareBeanPostProcessor)会在早期引用阶段返回代理对象,这样注入方拿到的就是代理,而不是裸对象。

  5. 注册为完全可用(addSingleton)

    • 初始化完成后,把最终实例放入一级缓存(singletonObjects),并清理二三级缓存中的相关项。


关键类 / 方法 / 作用(面试要点)

  • DefaultSingletonBeanRegistry:维护三缓存,并提供 addSingletonFactorygetSingletonregisterSingleton 等方法。

  • 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 无法或不建议处理循环依赖

  1. 构造器注入循环依赖:不支持(unless @Lazy 或 Provider)。

  2. prototype 作用域(多例):Spring 默认不做循环引用处理,若发现循环会抛异常;因为 prototype 的生命周期复杂,容器不会缓存早期引用。

  3. final 字段 / 不可变对象:需要在构造时传入的依赖无法通过 setter 注入解决。

  4. 复杂 AOP 场景或自定义 BeanPostProcessor 未正确实现 getEarlyBeanReference:可能导致注入方拿到不正确的早期引用或代理不一致。


如何在工程中避免或稳妥处理循环依赖(最佳实践)

  • 优先用构造器注入来表达必需依赖(更清晰、便于测试);但若构造器注入导致循环,说明设计上耦合度过高,应重构(抽取接口/合并组件/引入中间层)。

  • 对可选或延迟的依赖使用 @LazyObjectProvider<T> / javax.inject.Provider / ObjectFactory<T>。这能延迟获取并破环循环。

  • 用 setter 注入或字段注入(注意测试与可见性),让 Spring 能早期暴露引用。

  • 重构以减少互相依赖:抽取第三个共同依赖(Service C),把部分逻辑移到 C。

  • 避免把事务边界放在循环依赖的 bean 上,或谨慎处理 AOP 与代理问题。

  • 使用单例设计与职责单一原则:高内聚、低耦合,减少循环依赖的出现概率。


面试常见问法与示例回答(摘要)

  • 问:Spring 是怎么解决循环依赖的?
    答(要点):

    1. 只对 singleton 生效(prototype 不支持)。

    2. 利用 DefaultSingletonBeanRegistry三级缓存(singletonObjects / earlySingletonObjects / singletonFactories)在属性注入前提前暴露早期引用(early reference)

    3. doCreateBean 的 populate 阶段之前用 addSingletonFactory 注册工厂,其他 bean 请求时会通过 factory 得到早期引用。

    4. 对于代理(AOP)会通过 getEarlyBeanReference 返回代理,保证注入方拿到的是代理而不是裸对象。

    5. 构造器注入循环依赖不能解决,应改为延迟注入或重构。

  • 给出细节(提到类名/方法名/三缓存)会显著加分。


补充:示例说明(简短伪代码)

假设 A 依赖 B,B 依赖 A,且都是 singleton 且用字段/setter 注入:

  1. 创建 A:实例化 A(对象 created)→ addSingletonFactory("A", () -> getEarlyBeanReference(A)) → populate A(需要 B)

  2. 在 populate A 时需要 B,触发创建 B:实例化 B → 因为 singletonFactory("A") 存在,B 注入 A 时会通过 getSingleton("A") 得到 earlyReference(factory 创建可能是 proxy 或 raw) → B 注入 A 的引用继续完成 → B 初始化完成并注册为 singletonObjects。

  3. 返回到 A 的 populate,A 注入 B(已完成),A 初始化完成后把 A 放到 singletonObjects(并清理 early caches)。流程顺利,循环被打破。