spring默认禁止构造器注入的循环依赖,因无法提前暴露实例;仅支持单例bean在setter/字段注入下通过三级缓存解决,原型bean及隐式依赖(如事件监听、@postconstruct中getbean)仍会失败。

Spring 为什么默认禁止循环依赖
Spring 容器在单例 Bean 初始化过程中,会提前暴露一个“正在创建中”的 ObjectFactory 来应对循环依赖。但它只对「构造器注入」完全无解,仅支持「setter 或字段注入」场景下的三级缓存自救。
常见错误现象:BeanCurrentlyInCreationException 或启动时报 Requested bean is currently in creation;本质是 A 依赖 B、B 又在构造器里强依赖 A,Spring 连实例都还没 new 出来,根本没法塞进缓存。
- 构造器注入 + 循环依赖 → 必然失败,Spring 不做任何尝试
- 字段/Setter 注入 + 单例 + 非懒加载 → Spring 能靠
singletonObjects/earlySingletonObjects/singletonFactories三级缓存兜住 - 原型(
@Scope("prototype"))Bean 出现循环依赖 → 直接抛异常,不走缓存逻辑
如何识别代码里藏着的隐式循环依赖
不是只有 A → B → A 才叫循环依赖。当存在继承、代理、事件监听或自动装配泛型时,依赖链可能被遮蔽。
典型场景:
立即学习“Java免费学习笔记(深入)”;
- 使用
@EventListener订阅本类发布的事件 → 当前 Bean 尚未初始化完成,但事件机制已触发其方法调用 - 通过
ApplicationContext.getBean(Xxx.class)在@PostConstruct中手动获取另一个 Bean,而那个 Bean 又间接依赖当前类 - Lombok 的
@RequiredArgsConstructor自动生成构造器,若字段类型恰好形成闭环,编译期看不出,运行时报错
建议在 IDE 中右键查看 “Find Usages” 后逆向追踪依赖图,比光看 @Autowired 更可靠。
@Lazy 能不能一劳永逸解决循环依赖
加 @Lazy 是最常用的绕过手段,但它只是延迟初始化,并不消除依赖关系本身。
要注意:
-
@Lazy对构造器参数生效,但必须确保该 Bean 自身也是单例(否则每次 getBean 都新建,@Lazy失效) - 与
@Async或事务代理结合时,@Lazy可能导致代理对象未正确织入,出现空指针或事务不生效 - 测试环境下若用
Mockito.mock()替换 Bean,@Lazy会让 mock 行为失效——因为 Spring 拿到的是代理,不是原始 mock 实例
示例:如果 B 类有 @Lazy final A a;,那第一次调用 a.doSomething() 才触发 A 的创建,此时若 A 又依赖 C、C 又依赖 B,就又回到原问题。
真正该重构的不是配置,是职责边界
循环依赖往往暴露的是设计问题:两个类承担了不该耦合的职责,比如服务层直接操作数据源配置、监听器里混着业务逻辑、或者把策略选择和策略实现写死在同一处。
比起加 @Lazy 或拆成 setter,更值得花时间做的:
- 把共享状态抽成独立的
@Service,让 A 和 B 都依赖它,而非彼此 - 用
ApplicationEventPublisher替代直接调用,把“执行时机”和“执行主体”解耦 - 对复杂流程引入命令模式或状态机,避免服务类之间形成网状调用
Spring 的循环依赖支持是个补丁,不是设计指南。它能让你跑起来,但跑得久不久,取决于你有没有动过那几行真正该删掉的依赖。










