本文从实际项目问题出发深入讲解 Spring 循环依赖的产生原因、三级缓存设计思想、为什么不能使用两级缓存以及构造器注入和属性注入的区别。最后结合微服务架构分析循环依赖问题及最佳实践。一、前言最近在项目启动时遇到了这样一个异常BeanCurrentlyInCreationException: Bean with name thTakeoffMsgHandler has been injected into other beans [thPlanMsgHandle] in its raw version as part of a circular reference, but has eventually been wrapped.很多开发者看到这个异常第一反应都是Spring不是有三级缓存吗为什么还会循环依赖事实上这个问题涉及 Spring IOC、AOP 代理以及 Bean 生命周期是 Spring 框架中最经典的话题之一。本文将从源码设计思想出发彻底弄懂 Spring 为什么设计三级缓存以及在实际开发中如何避免循环依赖。二、什么是循环依赖循环依赖Circular Dependency就是两个或多个 Bean 相互依赖。例如A │ ▼ B │ ▼ A代码如下Service public class AService { Autowired private BService bService; }Service public class BService { Autowired private AService aService; }Spring 在创建 A 时需要先创建 B。而创建 B 时又需要 A。于是形成死循环。三、Spring Bean 创建流程Spring 创建 Bean 可以简化为下面几个步骤实例化对象(new) ↓ 属性注入(Autowired) ↓ Aware接口回调 ↓ BeanPostProcessor ↓ 初始化(PostConstruct) ↓ AOP代理 ↓ 放入一级缓存注意对象实例化和属性注入并不是同一个阶段。也正因为实例化先于依赖注入Spring 才有机会解决一部分循环依赖。四、Spring 为什么能解决循环依赖假设A ↓ B ↓ A创建过程第一步Springnew A()此时A 已经存在。但是Autowired PostConstruct 初始化都还没执行。第二步A 需要 B。Spring 去创建 B。第三步B 又需要 A。Spring 发现A 已经实例化了。于是把 A 提前暴露出去。B 就能继续创建。最后B 创建完成 ↓ A 注入 B ↓ A 创建完成整个流程结束。五、Spring 的三级缓存Spring 内部维护三个缓存。一级缓存singletonObjects作用存放已经完全初始化完成的 Bean。singletonObjects A完成 B完成所有 Bean 创建完成后都会进入这里。二级缓存earlySingletonObjects作用存放提前暴露的 Bean。例如new A()虽然Autowired PostConstruct 初始化还没有执行。但是可以先给其它 Bean 使用。三级缓存singletonFactories注意三级缓存里面放的不是 Bean。而是ObjectFactory也就是Bean工厂里面保存的是() - getEarlyBeanReference(beanName)它的作用就是需要的时候再决定返回普通对象还是代理对象。六、为什么不能只使用两级缓存这是 Spring 面试最经典的问题。答案因为 Spring 需要支持 AOP。假设Transactional public class OrderService { }真正放入 Spring 容器里的其实不是OrderService而是TransactionProxy(OrderService)也就是代理对象。如果只有两级缓存一级缓存 二级缓存那么创建 Bean 时new OrderService()只能提前暴露原始对象其它 Bean 拿到的也是OrderService而不是TransactionProxy那么事务就失效了。因为其它 Bean 调用的是原始对象。不是代理对象。三级缓存解决了这个问题。流程如下new OrderService() ↓ 放入三级缓存(ObjectFactory) ↓ 其它Bean需要OrderService ↓ 调用ObjectFactory ↓ getEarlyBeanReference() ↓ 如果需要代理 ↓ 提前生成代理对象 ↓ 放入二级缓存 ↓ 删除三级缓存 ↓ 其它Bean拿到代理对象 ↓ 最终一级缓存也是代理对象这样整个 Spring 容器中所有 Bean 引用的都是同一个代理对象。因此事务不会失效。七、三级缓存为什么不能直接存代理对象很多人会继续问三级缓存直接放代理对象不就行了吗答案是不行。原因有两个。原因一不是所有 Bean 都需要代理例如UserService没有Transactional Async Cacheable那么根本不用生成代理。如果Spring 一开始全部生成代理。会严重浪费性能。原因二代理对象创建成本较高Spring 使用JDK动态代理 CGLIB代理都需要解析注解 匹配Advisor 生成代理类 创建代理对象这些工作都比较耗时。所以Spring 采用懒生成只有真正需要的时候才调用ObjectFactory生成代理对象。八、构造器注入与属性注入有什么区别Spring 常见注入方式属性注入Service public class UserService { Autowired private OrderService orderService; }特点对象先创建。然后Spring 再注入依赖。构造器注入Service public class UserService { private final OrderService orderService; public UserService(OrderService orderService){ this.orderService orderService; } }特点创建对象之前依赖必须已经存在。否则无法创建对象。九、为什么构造器注入不能解决循环依赖来看流程。A需要BB需要A如果构造器注入new A(B)但是B 还没有。于是Spring 去创建 B。new B(A)结果A 也没有。因为A 连new都没完成。三级缓存根本没有机会保存 A。于是直接抛BeanCurrentlyInCreationException因此构造器注入无法利用三级缓存解决循环依赖。十、属性注入为什么可以属性注入流程new A() ↓ 放入三级缓存 ↓ 创建B ↓ B引用A ↓ 完成B ↓ 回到A ↓ 注入B ↓ 初始化完成因为A 已经实例化。Spring 才能提前暴露。所以三级缓存才能发挥作用。十一、微服务中的循环依赖很多人认为微服务之间也会出现 Spring 循环依赖。其实这是两个完全不同的问题。例如ServiceA ↓ Feign ↓ ServiceB这是HTTP调用【Feign/RestTemplate/Dubbo/gRPC】不是Bean注入所以不会出现BeanCurrentlyInCreationException真正可能出现的是业务循环。例如订单服务 ↓ 库存服务 ↓ 支付服务 ↓ 订单服务这属于RPC循环调用它会导致请求超时死循环服务雪崩熔断解决方式通常是MQ 异步解耦事件驱动Saga 分布式事务TCCOutbox Pattern而不是 Spring 的三级缓存。十二、实际开发推荐使用哪种注入方式很多开发规范如 Spring 官方推荐都更倾向于构造器注入原因包括依赖关系清晰创建对象时必须满足依赖。可以使用final字段提高对象不可变性。更方便单元测试容易注入 Mock 对象。能尽早暴露设计问题例如循环依赖。相比之下属性注入虽然书写简单但依赖关系不够显式也更容易在大型项目中形成隐藏耦合。需要注意的是不要因为构造器注入暴露了循环依赖就改回属性注入。真正应该做的是重新设计代码拆分职责而不是依赖三级缓存“帮忙解决”。十三、如何避免循环依赖如果项目中出现循环依赖建议优先考虑以下方案拆分公共逻辑将双方共同依赖的功能抽取到新的 Service 中。引入事件机制使用 Spring Event 或 MQ避免直接互相调用。重新划分职责一个类只负责一个业务方向减少双向依赖。必要时使用Lazy可以作为临时方案但不建议长期依赖。避免过度耦合如果两个 Service 长期互相调用通常意味着设计需要调整。对比项构造器注入字段/属性注入Autowired/Resource推荐程度更推荐兼容老项目较多是否支持final是否是否便于单元测试是一般是否容易发现设计问题是相对不容易Spring 能否利用三级缓存尝试解决循环依赖否可以但遇到代理 Bean 不一定成功十四、总结Spring 的三级缓存并不是为了“炫技”而是为了在支持循环依赖的同时保证 AOP 代理对象的一致性。理解三级缓存的关键可以归纳为三点一级缓存保存完全初始化完成的单例 Bean。二级缓存保存提前暴露的 Bean或提前暴露的代理对象。三级缓存保存ObjectFactory用于按需创建早期引用决定返回原始对象还是代理对象。在工程实践中更重要的是认识到循环依赖通常不是一个 Spring 技术问题而是一个软件设计问题。三级缓存只是 Spring 提供的一种兼容机制而不是鼓励开发者编写相互耦合的代码。真正优秀的系统应通过合理的职责划分、单向依赖和事件驱动等方式从设计层面避免循环依赖的产生。