
spring默认以单例(singleton)作用域管理bean,因此无论通过getbean()多次获取,还是在依赖注入过程中间接引用,同一类型bean始终复用唯一实例,这也是循环依赖能被成功解决的根本前提。
spring默认以单例(singleton)作用域管理bean,因此无论通过getbean()多次获取,还是在依赖注入过程中间接引用,同一类型bean始终复用唯一实例,这也是循环依赖能被成功解决的根本前提。
在你提供的示例中,DependencyA 和 DependencyB 均未显式声明作用域,因此遵循Spring默认规则——@Scope("singleton")。这意味着整个IoC容器中,每个Bean类有且仅有一个共享实例。这一特性直接解释了为何控制台中 "I am constructor of Dependency B" 仅输出一次:
- 当调用 applicationContext.getBean(DependencyA.class) 时,Spring开始创建 DependencyA 实例:
- 执行 DependencyA 构造函数 → 输出 "I am constructor of Dependency A";
- 发现需注入 DependencyB,于是触发 DependencyB 的创建流程;
- 执行 DependencyB 构造函数 → 输出 "I am constructor of Dependency B";
- 完成 DependencyB 的属性填充(此时 setDependencyA 尚未执行,因 DependencyA 尚未完全就绪);
- 将早期暴露的 DependencyB 引用(通过三级缓存机制)注入 DependencyA;
- 继续完成 DependencyA 的 setDependencyB 方法调用 → 输出 "I am setDependencyB !";
- 随后调用 applicationContext.getBean(DependencyB.class):
- Spring查找到已创建并缓存的 DependencyB 单例实例,直接返回,不再重复构造;
- 因此 DependencyB 构造函数不会再次执行,"I am constructor of Dependency B" 不会重复打印;
- 但 setDependencyA 方法会在首次创建 DependencyB 时完成注入(此时 DependencyA 已部分就绪),故输出 "I am setDependencyA !"。
这背后依赖的是Spring经典的三级缓存机制:
| 缓存层级 | 名称 | 存储内容 | 关键作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化完毕的成品Bean(已执行完所有生命周期回调) | 对外提供最终可用的单例Bean |
| 二级缓存 | earlySingletonObjects | 提前曝光的半成品Bean(已完成实例化、未完成属性填充) | 快速提供早期引用,避免重复创建 |
| 三级缓存 | singletonFactories | ObjectFactory 工厂对象 | 按需生成早期Bean,支持AOP代理等动态增强逻辑 |
正是通过三级缓存协同工作,Spring能在 DependencyA 尚未完全初始化时,将其“早期引用”放入缓存;当 DependencyB 创建时,从缓存中取出该引用完成注入,从而打破构造顺序死锁——但这一切的前提是Bean必须为单例。
⚠️ 注意事项:
- 构造器注入不支持循环依赖:因构造阶段无法提供早期引用,三级缓存无法介入;
- Prototype Bean不参与缓存:每次 getBean() 都新建实例,无共享机制,故无法解决其循环依赖;
- Spring 6.0+ / Spring Boot 3.x 默认禁用循环引用:需显式配置 spring.main.allow-circular-references=true 才启用该能力;
- 若需验证原型行为,可在任一Bean上添加 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE),此时构造函数将每次调用都执行。
综上,你的输出现象并非异常,而是Spring单例作用域与三级缓存协同工作的标准表现——它保障了Bean复用性、内存效率,也构成了循环依赖得以优雅化解的底层基石。











