1. 循环依赖的本质与SpringBoot中的表现
循环依赖问题在SpringBoot项目中就像两个互相等待对方先伸手的舞伴——A对象需要B对象完成初始化才能构建自己,而B对象又需要A对象先准备好才能继续。这种死锁般的场景在实际开发中并不罕见,特别是随着项目规模扩大和模块增多时。
Spring框架处理依赖注入的核心机制是通过三级缓存来解决循环依赖的。具体来说:
- 第一级缓存存放完全初始化好的bean
- 第二级缓存存放早期暴露的bean引用(已实例化但未完成属性注入)
- 第三级缓存存放bean工厂对象
当遇到A→B→A这样的依赖链时,Spring会先创建A的原始对象放入二级缓存,然后在注入B时发现需要A,此时能从缓存中拿到A的引用,避免无限循环。这种机制对setter注入和字段注入有效,但对构造器注入无能为力——因为构造器注入必须在实例化阶段就完成所有依赖项的注入。
关键提示:SpringBoot 2.6.x版本开始默认禁止循环依赖,这是Spring团队为了推动更好代码实践做出的改变。可以通过
spring.main.allow-circular-references=true配置重新启用,但这只是临时解决方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 循环依赖的典型场景与诊断方法
2.1 常见产生场景
- 分层架构中的双向调用:ServiceA调用ServiceB的方法,同时ServiceB又需要回调ServiceA的接口
- 事件监听模式:监听器与被监听对象相互持有引用
- 缓存场景:缓存管理器与业务服务相互依赖
- 配置类交叉引用:@Configuration类之间相互@Autowired
2.2 诊断工具与异常分析
当出现循环依赖时,Spring通常会抛出BeanCurrentlyInCreationException。通过分析异常堆栈可以定位循环链:
code复制Requested bean is currently in creation:
Is there an unresolvable circular reference?
更直观的方法是使用Spring Boot Actuator的/beans端点,它会显示所有bean的依赖关系图。对于复杂项目,推荐使用ArchUnit编写架构测试来预防循环依赖:
java复制@ArchTest
static final ArchRule no_cycles =
slices().matching("com.yourpackage.(*)").should().beFreeOfCycles();
3. 根治循环依赖的六大设计策略
3.1 接口抽象解耦法
将相互依赖的部分提取为接口,让实现类仅依赖接口而非具体实现。例如:
java复制// 改造前
@Service
class OrderService {
@Autowired UserService userService;
}
@Service
class UserService {
@Autowired OrderService orderService;
}
// 改造后
public interface OrderDependent {
void processOrder(Order order);
}
@Service
class OrderService implements OrderDependent {
@Autowired UserService userService;
}
@Service
class UserService {
@Autowired @Qualifier("orderService") OrderDependent orderDependent;
}
3.2 事件驱动模式
使用ApplicationEvent实现松耦合通信:
java复制@Service
class OrderService {
@Autowired ApplicationEventPublisher publisher;
public void createOrder() {
publisher.publishEvent(new OrderCreatedEvent(this, order));
}
}
@Service
class UserService {
@EventListener
public void handleOrderEvent(OrderCreatedEvent event) {
// 处理订单事件
}
}
3.3 延迟注入技巧
通过ObjectProvider实现按需获取:
java复制@Service
class OrderService {
private final ObjectProvider<UserService> userServiceProvider;
public OrderService(ObjectProvider<UserService> provider) {
this.userServiceProvider = provider;
}
public void process() {
UserService userService = userServiceProvider.getIfUnique();
// 使用userService
}
}
3.4 方法调用替代字段注入
将部分依赖改为方法参数传递:
java复制@Service
class OrderService {
public void confirmOrder(Order order, UserService userService) {
// 使用传入的userService
}
}
3.5 三级缓存原理的应用
理解Spring的三级缓存机制后,可以合理设计bean的初始化顺序:
java复制@Component
class BeanA {
@Autowired BeanB beanB;
@PostConstruct
public void init() {
// 需要BeanB但不在构造阶段使用
}
}
@Component
class BeanB {
@Autowired BeanA beanA;
}
3.6 模块重组与职责重构
从根本上重新设计类职责,例如:
改造前:
- UserService负责用户管理和用户订单统计
- OrderService负责订单处理和用户积分计算
改造后:
- UserService专注用户CRUD
- OrderService专注订单CRUD
- UserStatisticsService负责统计相关逻辑
- PointsService负责积分计算
4. SpringBoot特定场景的解决方案
4.1 @Lazy注解的巧妙使用
在循环依赖的一方使用@Lazy实现延迟加载:
java复制@Service
class OrderService {
private final UserService userService;
@Autowired
public OrderService(@Lazy UserService userService) {
this.userService = userService;
}
}
4.2 配置类循环依赖处理
配置类之间的循环依赖需要特殊处理:
java复制@Configuration
class ConfigA {
@Bean
@DependsOn("beanB")
public BeanA beanA() {
return new BeanA();
}
}
@Configuration
class ConfigB {
@Bean
public BeanB beanB(BeanA beanA) {
return new BeanB(beanA);
}
}
4.3 Spring Boot 2.6+的应对策略
对于新版Spring Boot的默认禁止行为,除了全局配置外,还可以:
- 使用@DependsOn明确依赖顺序
- 重构为构造器注入+@Lazy组合
- 采用4.0中提到的各种解耦模式
5. 生产环境中的实战案例
5.1 支付系统循环依赖破解
某支付系统存在以下循环:
- PaymentService → TransactionService
- TransactionService → NotificationService
- NotificationService → PaymentService
解决方案:
- 将通知功能抽象为PaymentEventPublisher接口
- TransactionService通过事件通知支付状态
- NotificationService监听事件而不直接依赖PaymentService
5.2 微服务本地缓存陷阱
在商品服务中:
- ProductService → LocalCacheManager
- LocalCacheManager → ProductService (用于缓存失效时重新加载)
重构方案:
java复制// 改造后的缓存管理器
public class ProductCacheManager {
private final ProductLoader productLoader;
interface ProductLoader {
Product loadProduct(String id);
}
}
// ProductService实现ProductLoader接口
5.3 监控系统的依赖死锁
监控系统中:
- MetricCollector → HealthChecker
- HealthChecker → MetricCollector (用于收集健康指标)
最终方案:
采用反应式编程模型,通过MeterRegistry发布指标数据,双方都监听事件总线而非直接引用。
6. 预防循环依赖的工程化实践
6.1 架构规范制定
- 强制规定依赖方向:Controller → Service → Repository
- 禁止同级服务相互调用
- 跨层调用必须通过接口
6.2 静态代码分析
在CI流水线中加入架构测试:
java复制@ArchTest
public static final ArchRule layer_dependencies =
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayOnlyBeAccessedByLayers("Service")
.whereLayer("Service").mayOnlyBeAccessedByLayers("Repository");
6.3 依赖可视化工具
使用Spring Boot Dependency Graph插件或执行:
bash复制mvn dependency:tree -Dincludes=org.springframework
6.4 代码审查要点
重点关注:
- 构造器注入中的复杂依赖
- @PostConstruct方法中的跨bean调用
- 配置类之间的相互@Bean引用
- 监听器与被监听对象的直接耦合
在实际项目中,我倾向于采用"接口隔离+事件驱动"的组合方案。特别是在微服务架构中,事件驱动不仅能解决循环依赖,还能为未来的服务拆分预留扩展点。对于遗留系统的改造,ObjectProvider和@Lazy是相对安全的过渡方案,但最终目标应该是通过领域重构实现清晰的依赖关系。
