A 依赖 B,B 依赖 A:报错从哪来
上上篇讲过三级缓存:实例化 → 提前暴露工厂 → 属性注入。注意顺序——能救循环依赖的前提,是对象先实例化成功。换成构造器注入,故事在第一步就终结:A 创建时要先拿到 B 当构造参数,B 创建时要先拿到 A,两个都卡在实例化,缓存里什么都没来得及放,Spring 只能抛 BeanCurrentlyInCreationException。记住这个判据:能过实例化这一关的循环,三级缓存都能救;过不了的,谁也救不了。
Boot 2.6 的转折:默认禁止
Spring Boot 2.6 起,循环依赖默认禁止(spring.main.allow-circular-references 默认 false),启动直接失败并打印引用链。为什么收紧?因为循环依赖多数是设计问题的遮羞布——两个类互相调来调去,职责早就搅在一起了。老项目升级后启动炸一片,多半就是这个默认值在作怪。开关可以打开,但那是续命不是治病,先看下面三招怎么拆。
三招实战拆解
第一招 @Lazy,应急首选。在注入点标 @Lazy,Spring 注入的不是真 Bean 而是一个代理,首次调用方法时才去容器里拿真身——把「创建时就要」改成「第一次用时才要」,时机错开,环自然解开。这和三级缓存的工厂思想同源:都是延迟交付。
@Service
public class OrderService {
private final InventoryService inventoryService;
// 注入的是延迟代理,首次调用方法时才真正初始化
public OrderService(@Lazy InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
}第二招 ObjectProvider,更体面的延迟。不要具体对象,要一个「提供者」,用的时候才 getObject()——语义清晰,还能遍历拿所有实现,适合「有这个依赖但不急」的场景。
@Service
public class OrderService {
private final ObjectProvider<InventoryService> inventory;
public OrderService(ObjectProvider<InventoryService> inventory) {
this.inventory = inventory;
}
public void create() {
// 用的时候才解析依赖
inventory.getObject().lockStock();
}
}第三招重构,根治。循环依赖九成是职责没拆干净:把两个类共用的逻辑抽成第三个类 C,A 和 B 都依赖 C,环断成链。别嫌土,这是唯一不欠技术债的解法。
死结为什么解不开
把构造器的死结钉死在原理上:三级缓存的救命点是「实例化之后、属性注入之前」把工厂塞进缓存——前提是对象已经 new 出来了。构造器注入里,new 这一步本身就需要依赖在场,鸡生蛋蛋生鸡,缓存没有出场机会。口诀:字段与 setter 的循环靠缓存或 @Lazy,构造器的循环只能拆类或延迟。另外上上篇提过的 @Async 是特例,早期暴露的原始对象和最终代理对不上号,字段注入的环它也救不了——这种环别救,拆。
下一篇换个解耦姿势:Spring 事件机制。一次发布、多处响应,还有 @TransactionalEventListener 解决「事务没提交就收到通知」的时序暗坑。
评论 (0)