SpringBoot循环依赖

以下是针对三个Spring Bean循环依赖问题(A→B→C→A)的解决方案,结合Lombok实现,并分析各方案的优缺点:

方案一:@Lazy延迟加载

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Lazy;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ServiceA {
private final @Lazy ServiceB serviceB;
private final @Lazy ServiceC serviceC;
}

@Service
@RequiredArgsConstructor
public class ServiceB {
private final @Lazy ServiceA serviceA;
private final @Lazy ServiceC serviceC;
}

@Service
@RequiredArgsConstructor
public class ServiceC {
private final @Lazy ServiceA serviceA;
private final @Lazy ServiceB serviceB;
}

优点

  • 代码简洁,仅需添加@Lazy注解
  • 保持构造器注入的不可变性
  • 对原有业务逻辑无侵入

缺点

  • 运行时生成代理对象,有轻微性能开销
  • 可能隐藏设计问题,长期维护存在隐患
  • 调试时可能因代理对象导致断点异常

方案二:Setter注入+@PostConstruct

import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;

@Service
public class ServiceA {
@Setter(onMethod_ = @Autowired)
private ServiceB serviceB;

@Setter(onMethod_ = @Autowired)
private ServiceC serviceC;

@PostConstruct
public void init() {
// 验证依赖注入完成
}
}

优点

  • 符合Spring三级缓存机制
  • 依赖关系明确
  • 可在@PostConstruct中验证依赖状态

缺点

  • 打破构造器注入的不可变性
  • 需额外维护初始化逻辑
  • @PostConstruct中调用依赖方法,仍可能触发循环

方案三:ApplicationContextAware手动获取

import lombok.Getter;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;

@Service
public class ServiceA implements ApplicationContextAware {
@Getter
private ServiceB serviceB;

@Getter
private ServiceC serviceC;

@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.serviceB = context.getBean(ServiceB.class);
this.serviceC = context.getBean(ServiceC.class);
}
}

优点

  • 完全控制依赖获取时机
  • 不依赖Spring的缓存机制
  • 适用于复杂依赖场景

缺点

  • 代码侵入性强,与Spring API耦合
  • 破坏依赖注入的声明式特性
  • 手动管理依赖可能导致NPE

方案四:事件驱动解耦

import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ServiceA {
private final ApplicationEventPublisher publisher;

public void doSomething() {
publisher.publishEvent(new AEvent(this));
}

@EventListener
public void handleBEvent(BEvent event) {
// 处理B的响应
}
}

优点

  • 完全消除循环依赖
  • 实现松耦合的消息传递
  • 支持异步处理提升性能

缺点

  • 增加事件类和监听逻辑
  • 同步调用转为异步模型,复杂度上升
  • 调试链路变长,排查问题困难

方案五:重构设计(推荐)

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CoordinatorService {
private final ServiceA serviceA;
private final ServiceB serviceB;
private final ServiceC serviceC;

public void orchestrate() {
serviceA.doA();
serviceB.doB();
serviceC.doC();
}
}

@Service
@RequiredArgsConstructor
public class ServiceA {
private final CoordinatorService coordinator;
}

优点

  • 从根本上消除循环依赖
  • 依赖关系清晰,符合单一职责
  • 便于单元测试和维护

缺点

  • 需要重新设计架构
  • 可能涉及大量代码改动
  • 短期实现成本较高

方案对比表

方案实现难度性能影响代码侵入性可维护性适用场景
@Lazy轻微中等快速修复,临时方案
Setter注入中等依赖关系较简单
ApplicationContextAware复杂场景,特殊需求
事件驱动中等高并发,需异步处理
重构设计最高最高长期维护,架构优化

最佳实践建议

  1. 优先重构设计:循环依赖通常是设计缺陷的信号,尝试提取公共逻辑到中间组件
  2. 临时方案用@Lazy:在紧急修复时使用,但需标记为技术债务
  3. 避免深层嵌套调用:每个Service调用其他Service不超过1层,防止StackOverflowError
  4. 监控代理开销:对性能敏感的服务,使用@Lazy时需评估代理对象的影响
  5. 单元测试覆盖:确保循环依赖的路径被测试用例覆盖,验证稳定性

通过合理选择方案,可以在解决循环依赖的同时,保持代码的可维护性和性能。