1. 循环依赖的本质与危害
循环依赖就像两个互相等待对方先伸手的舞者——ServiceA 依赖 ServiceB 的同时,ServiceB 又反向依赖 ServiceA。这种双向依赖关系在Spring框架中会引发经典的"鸡生蛋蛋生鸡"问题。我曾在电商系统中遇到过真实的案例:订单服务调用库存服务检查库存,而库存服务又需要调用订单服务获取预售数据,启动时直接导致Bean创建失败。
从Spring容器启动流程来看,循环依赖会导致三级缓存机制失效。具体表现为:
- 容器尝试创建BeanA时发现需要注入BeanB
- 转而创建BeanB时又发现需要注入BeanA
- 此时BeanA尚未完成初始化,形成死锁
这种设计缺陷带来的运行时风险包括:
- 启动阶段直接抛出BeanCurrentlyInCreationException
- 即使通过某些方式启动成功,方法调用时可能出现NPE
- 单元测试难以覆盖复杂的依赖调用链
- 系统扩展时依赖关系会像蜘蛛网一样越来越复杂
关键提示:循环依赖最危险的不是编译期报错,而是运行时可能出现的不可预知行为。我曾见过一个生产系统在流量激增时,因循环依赖导致的服务雪崩。
2. Spring处理循环依赖的底层机制
2.1 三级缓存工作原理
Spring通过三级缓存解决setter注入的循环依赖问题,其核心流程如下:
// 伪代码展示三级缓存运作 public Object getBean(String name) { // 1. 检查一级缓存(完整Bean) Object singleton = getSingleton(name); if (singleton != null) return singleton; // 2. 标记Bean为创建中 beforeSingletonCreation(name); // 3. 实例化(分配内存) Object instance = createBeanInstance(beanDefinition); // 4. 加入三级缓存(原始对象) addSingletonFactory(name, () -> getEarlyBeanReference(beanName, beanDefinition, instance)); // 5. 属性填充(可能触发循环依赖) populateBean(instance); // 6. 初始化 initializeBean(instance); // 7. 移入一级缓存 addSingleton(name, instance); return instance; }2.2 构造器注入为何失效
构造器注入无法使用三级缓存方案,因为:
- 实例化阶段就需要完整依赖对象
- 没有机会先暴露ObjectFactory
- 必须所有依赖都就绪才能完成构造
测试用例演示:
// 会抛出BeanCurrentlyInCreationException @Service class ServiceA { private final ServiceB b; public ServiceA(ServiceB b) { this.b = b; } } @Service class ServiceB { private final ServiceA a; public ServiceB(ServiceA a) { this.a = a; } }3. 循环依赖的六种解决方案
3.1 代码重构方案
方案一:接口抽离(推荐)
// 公共接口 interface InventoryChecker { boolean checkInventory(Long sku); } // 实现类A @Service class OrderService implements InventoryChecker { @Autowired private InventoryService inventoryService; public boolean checkInventory(Long sku) { // 实现逻辑 } } // 实现类B @Service class InventoryService { @Autowired private InventoryChecker inventoryChecker; }方案二:方法抽取将相互依赖的部分抽到新类中:
@Service class DependencyHolder { @Autowired private OrderService orderService; @Autowired private InventoryService inventoryService; public void init() { orderService.setDependency(this); inventoryService.setDependency(this); } }3.2 Spring技术方案
方案三:@Lazy注解
@Service class OrderService { @Lazy @Autowired private InventoryService inventoryService; }方案四:Setter注入
@Service class OrderService { private InventoryService inventoryService; @Autowired public void setInventoryService(InventoryService s) { this.inventoryService = s; } }3.3 架构级方案
方案五:事件驱动
// OrderService @EventListener public void handleInventoryEvent(InventoryEvent event) { // 处理库存变更 } // InventoryService public void updateStock() { applicationContext.publishEvent(new InventoryEvent(data)); }方案六:门面模式
@Service class FacadeService { @Autowired private OrderService orderService; @Autowired private InventoryService inventoryService; public OrderResult placeOrder() { // 协调两个服务调用 } }4. 生产环境最佳实践
4.1 检测与预防
使用ArchUnit进行架构测试:
@ArchTest void noCyclicDependencies(JavaClasses classes) { slices().matching("com.yourapp.(*)..") .should().beFreeOfCycles(); }Spring Boot启动时增加检查:
spring.main.allow-circular-references=false4.2 性能影响评估
通过JMH测试不同方案的吞吐量:
Benchmark Mode Cnt Score Error Units LazyResolutionBenchmark.direct avgt 5 ≈100ns ops/ns LazyResolutionBenchmark.lazy avgt 5 ≈350ns ops/ns4.3 典型场景决策树
graph TD A[发现循环依赖] --> B{是否必须双向调用?} B -->|是| C[采用事件驱动] B -->|否| D[接口抽离] C --> E[评估性能损耗] D --> F[验证解耦效果]5. 疑难问题排查指南
Case 1:AOP代理导致的循环依赖
现象:使用@Async/@Transactional时循环依赖报错 解决方案:
@Autowired public void setDependency(@Lazy OtherService otherService) { this.otherService = otherService; }Case 2:多线程环境下的NPE
根本原因:@Lazy代理对象在并发下可能未初始化 修复方案:
private final Object lock = new Object(); public void doWork() { synchronized(lock) { // 使用依赖对象 } }Case 3:单元测试失败
测试配置要点:
@TestConfiguration static class TestConfig { @Bean @Primary ServiceA mockA() { return mock(ServiceA.class); } }6. 架构视角的根治方案
6.1 模块化拆分
采用领域驱动设计:
src/ ├── order/ │ ├── application/ │ ├── domain/ │ └── infrastructure/ └── inventory/ ├── application/ └── domain/6.2 依赖治理工具
使用Maven Enforcer插件:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <executions> <execution> <id>enforce</id> <goals><goal>enforce</goal></goals> <configuration> <rules> <banCircularDependencies/> </rules> </configuration> </execution> </executions> </plugin>6.3 持续集成检查
Jenkins流水线配置:
stage('Static Analysis') { steps { sh 'mvn org.archunit:archunit-maven-plugin:analyze' } }在实际项目中,我倾向于采用"接口抽离+事件驱动"的组合方案。对于核心域服务保持严格单向依赖,对于跨域协作采用事件通知机制。这种架构经过多个百万级DAU系统验证,既能保持代码清晰度,又能满足复杂业务场景的需求。