☰
构造器注入一启动就报 BeanCurrentlyInCreationException:三级缓存救不了的场景,我画完图才看懂
2026/10/1 15:33:23 网站建设 项目流程

title: 构造器注入一启动就报 BeanCurrentlyInCreationException:三级缓存救不了的场景,我画完图才看懂
date: 2026-10-01
tags: [Spring, 循环依赖, 三级缓存, AOP, 源码解析, Java]


2025 年初我们做订单服务重构,两个类互相依赖:OrderService依赖InventoryService做扣减,InventoryService又要回调OrderService更新状态。同事为了图省事全用了构造器注入,服务一启动就抛BeanCurrentlyInCreationException。他把这两个类改成 @Autowired 字段注入,问题消失了,但留言问我:不是说 Spring 有三级缓存能解决循环依赖吗?为什么构造器注入就不行?

这个问题我当年也被问倒过。后来把AbstractAutowireCapableBeanFactory的doCreateBean源码从头到尾走了一遍,才真正理解三级缓存的边界:它解决的是"实例化之后、初始化之前"的依赖暴露问题,而构造器注入的依赖发生在实例化内部,连"先造出半成品"的机会都没有。

一、事故现场:两个类互相要,谁也生不出来

最小复现代码:

@Service public class OrderService { private final InventoryService inventoryService; // 构造器注入:必须先拿到 InventoryService 才能实例化自己 public OrderService(InventoryService inventoryService) { this.inventoryService = inventoryService; } public void deductAndComplete(Long orderId) { inventoryService.deduct(orderId); } } @Service public class InventoryService { private final OrderService orderService; public InventoryService(OrderService orderService) { this.orderService = orderService; } public void deduct(Long orderId) { orderService.markCompleted(orderId); } }

启动时 Spring 的执行顺序:

// Spring 创建 bean 的主干逻辑(简化) protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) { // 第 1 步:实例化 —— 调用构造器,此时还没进三级缓存 BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args); Object bean = instanceWrapper.getWrappedInstance(); // 第 2 步:把半成品放进三级缓存(实例化之后才轮到这里) boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } // 第 3 步:populateBean 注入属性、initializeBean 执行初始化 populateBean(beanName, mbd, instanceWrapper); }

关键在第 1 步和第 2 步的先后顺序。创建OrderService时要先执行它的构造器,构造器参数需要InventoryService,于是 Spring 转头去创建InventoryService,而InventoryService的构造器又需要OrderService——此时OrderService连实例都没 new 出来,三级缓存里没有任何可以给对方的东西。Spring 检测到OrderService处于"创建中"状态又拿不到早期引用,只能抛异常终止启动。

我们当时的报错栈里有一行Requested bean is currently in creation,指向的正是DefaultSingletonBeanRegistry里的beforeSingletonCreation校验。

二、三级缓存到底存了什么

字段定义在DefaultSingletonBeanRegistry:

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry { // 一级缓存:成品 bean,key 是 beanName private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 二级缓存:提前暴露的半成品(实例化了但没填充属性) private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 三级缓存:能产出半成品的工厂 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); }

逐行解释:
- 一级缓存是所有单例 bean 的最终归宿,日常getBean命中的就是它。
- 二级缓存存的是"提前曝光"的引用:bean 实例化完成、属性还没注入时的原始对象。
- 三级缓存存的是 ObjectFactory,调用它才会得到(可能被 AOP 包装的)早期引用,结果会晋升到二级缓存,同时三级缓存里的工厂被移除。

查找逻辑在getSingleton:

protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 先查一级缓存,成品直接返回 Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 一级没有且该 bean 正在创建中,才继续查早期引用 singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { // 最后查三级缓存工厂 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); // 工厂产出的对象晋升到二级缓存 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } return singletonObject; }

回到事故场景:如果两个类都改成字段注入(setter 注入同理),流程就变成——实例化OrderService(不传参,new 空壳成功)→ 半成品进三级缓存 → 填充属性时发现要InventoryService→ 实例化InventoryService→ 它填属性时要OrderService→ 从三级缓存拿到OrderService的半成品引用 → 自己完成初始化 → 回过头OrderService拿到完整的InventoryService继续初始化。环就这么解开了。

三、为什么要三级,二级不够吗

这是面试高频题,但我不打算背标准答案,我讲一下我自己推导的过程。

假设只有二级缓存(早期引用池)。没有 AOP 时确实够用:半成品放进去,对方拿走引用,最后初始化完成把成品覆盖进一级缓存。引用是同一个,没问题。

有 AOP 时问题来了。正常流程下,代理对象是在初始化最后一步由BeanPostProcessor的postProcessAfterInitialization生成的,成品池里放的应该是代理。但如果循环依赖发生在代理生成之前,对方从二级缓存拿到的只能是原始对象——这个原始对象会被对方长期持有,和最终容器里的代理对象不是同一个实例,事务、缓存注解全部失效。

三级缓存的 ObjectFactory 就是为此设计的:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessors()) { // AOP 的 AbstractAutoProxyCreator 实现了这个接口 // 提前在此生成代理,而不是等到初始化之后 exposedObject = bp.getEarlyBeanReference(exposedObject, beanName); } return exposedObject; }

逐行看:工厂被调用时,遍历所有支持早期引用的后置处理器,AOP 的AbstractAutoProxyCreator在这里提前判断这个 bean 是否需要代理,需要就当场生成代理返回。这样对方拿到的引用和最终成品一致。

那为什么不直接在实例化后统一生成代理放进二级缓存?因为代理生成的前提条件(比如 @Transactional 的 Advisor 匹配、循环依赖是否存在)在早期阶段并不完备,Spring 的设计原则是"没有循环依赖就按标准生命周期走,有才提前"。三级缓存的工厂相当于一个惰性开关:没有环,工厂永远不被调用,代理照常在初始化后生成;有环,工厂被触发,代理提前生成。二级缓存方案无法表达这种"按需提前"。

四、三个三级缓存救不了的场景

我总结成三条硬边界,画在白板上传给了团队:

  1. 构造器注入的循环依赖。依赖发生在实例化内部,对象不存在,没有"半成品"可暴露。解法是其中一个改成 setter 注入,或者在依赖方加@Lazy让 Spring 注入一个延迟代理。

  2. prototype 作用域的循环依赖。三级缓存只服务单例,prototype 每次都新建,Spring 不缓存也不追踪,直接抛异常。

  3. 跨层初始化顺序敏感的场景。比如 A 的 @PostConstruct 里依赖 B 已完全初始化,而 B 又要提前引用 A——提前暴露的 A 是半成品,B 在回调里调用 A 的方法时字段可能还是 null。我们在库存服务里就撞过一次:回调链上读了一个注入进来的配置对象,因为走的是早期引用,读到了 null,NPE 挂在启动后的第一次请求里,比启动失败更难查。

第三条是我观点最重的一条:能自己解决的循环依赖,别指望容器兜底。我当时推动团队把这两个服务之间的回调改成了事件发布(ApplicationEvent),InventoryService扣减后发事件,OrderService监听更新状态,依赖方向变成单向,环直接消失。改完之后启动快了大概 3 秒(少了两次代理提前生成的开销),更重要的是代码不再隐式依赖"Spring 会帮我解环"这个魔法。

五、复盘真实数字

  • 事故影响:订单服务启动失败,发版阻塞 40 分钟
  • 根因:构造器注入形成实例化期环,三级缓存无能为力
  • 修复:改为事件解耦,消除双向依赖
  • 后续收益:同类问题在该团队此后一年零复发
  • 排查成本:画依赖图 15 分钟 + 读 doCreateBean 源码约 2 小时

六、思考题

你的项目里有多少个 @Autowired 字段注入?试着把其中一个改成构造器注入,观察启动行为有什么变化——如果没报错,说明这条依赖链上没有环;如果报错了,恭喜你提前发现了一处隐式循环依赖。欢迎在评论区聊聊你拆环的方法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询