用Spring的时候怎么避免循环依赖?

1 个回答

  • 鬼魅森林
    为了击碎你伪装的表面,我只能用尽全身力气挥出那惊鸿的半月弧光。
    **循环依赖要尽量通过设计消除**;Spring 只对**单例 + 非构造器注入**的场景用“提前暴露引用(三级缓存)”做了兜底,其他大多不行或有隐患。推荐用**构造器注入让问题暴露**,再用重构/解耦手段解决;万不得已再用 `@Lazy`/`ObjectProvider` 等“延迟拿 Bean”的技巧。 # 一、Spring 能/不能解的情况 | 场景 | 是否可解 | 说明 | | -------------------------------- | ---- | --------------------------------------- | | 单例 A ↔ B,**字段/Setter 注入** | ✅ | Spring 用“早期引用”临时放入缓存,能注入成功 | | 单例 A ↔ B,**构造器注入** | ❌ | 构造器还没生成实例,无法提前暴露 | | **原型**(prototype)循环 | ❌ | 原型不进单例缓存,无解 | | **不同作用域**(如 request ↔ singleton) | ❌ | 作用域不一致,容易走空或代理,设计应避免 | | 涉及 **@Transactional/@Async** 等代理 | ⚠️ | 大多单例字段注入可解;但构造器循环/复杂链可能失败或产生“半成品”引用,需谨慎 | > 说明:Spring Boot 某些版本默认**禁用循环引用**(`spring.main.allow-circular-references=false`)。即便能开启,也不建议依赖这个机制。 # 二、优先级最高的解决思路(推荐) 1. **重构打断环** * 把 A、B 的互相调用抽到第三个服务 **C**(或 Facade/DomainService)里: A → C,B → C,而不是 A ↔ B。 * 复用的那份逻辑下沉到**更低层**(如 Repository/Helper)。 2. **事件/消息解耦**(同步硬依赖改成异步通知) * A 发布事件,B 监听:`ApplicationEventPublisher` + `@EventListener`。 * 适合“完成动作后通知”的场景(如“订单创建完成”→“发送优惠券”)。 3. **端口-适配器(接口解耦)** * B 依赖的是 A 暴露的**输出接口(Port)**,实现放到独立 Bean,避免把整个 A 注入到 B。 * 只传必需能力,削减耦合边界。 4. **避免在构造器里做工作** * 构造器只接收依赖,不要在构造器里**调用另一个 Bean 的方法**;需要初始化逻辑放到 `@PostConstruct`/显式 `init()`。 5. **统一规则** * **层次化依赖**:Controller → Service → Repository;禁止 Service ↔ Service 互调(用 Facade/DomainService 协调)。 # 三、实在需要“延迟拿 Bean”时(权衡用) * **`@Lazy`(推荐其一)**:让对方以代理形式延迟获取,避免构造期死锁。 ```java @Service class A { private final B b; A(@Lazy B b){ this.b=b; } } @Service class B { private final A a; B(@Lazy A a){ this.a=a; } } ``` 注意:**不要在构造器里立即使用 b/a**,会触发过早初始化。 * **`ObjectProvider<T>` / `Provider<T>`(更显式)**:需要时再取。 ```java @Service class A { private final ObjectProvider<B> bProvider; A(ObjectProvider<B> bProvider){ this.bProvider=bProvider; } void doWork(){ B b = bProvider.getIfAvailable(); /* 使用 */ } } ``` * **接口拆分 + 延迟获取**:只注入对方**轻量接口**;或把“回调”做成 `Consumer`/`Supplier` 传入。 > 这些手段属于“战术止血”,长期还是应**消除结构性环**。 # 四、定位与守护 * **首选构造器注入**:能**早失败**,逼出环,避免 Spring 靠早期引用“遮住问题”。 * 看到异常 “`Requested bean is currently in creation`”、“`circular reference`” 就去画依赖图。 * 如果确有历史包袱,临时开开关: `application.yml` → `spring.main.allow-circular-references=true`(仅作为过渡期手段)。 * 注意 **自调用** 与 AOP:`@Transactional` 等需要**经由代理**调用;把互调拆到独立 Bean,避免“自调用失效”。 # 五、两段式示例(重构前后) **坏例子(A ↔ B):** ```java @Service class A { @Autowired B b; void a(){ b.b(); } } @Service class B { @Autowired A a; void b(){ a.a(); } } ``` **重构(引入 C):** ```java @Service class C { void core(){ /* 原 A/B 交叉逻辑放这里 */ } } @Service class A { private final C c; A(C c){ this.c=c; } void a(){ c.core(); } } @Service class B { private final C c; B(C c){ this.c=c; } void b(){ c.core(); } } ``` **结论**: 根治循环依赖靠**设计层面的拆分和解耦**;日常开发用**构造器注入**尽早暴露问题;必要时用 `@Lazy`/`ObjectProvider` 临时过渡,但要尽快回到**无环**的依赖图。
    发布于 2025-08-18 17:05 •IP属地 青海
    举报
    评论
    收藏