循环依赖

循环依赖(Circular Dependency)指两个或多个 Bean 相互依赖,形成闭环。Spring 能解决部分循环依赖,但 Spring Boot 2.6 起默认禁止循环依赖,鼓励通过设计消除。


问题表现

BeanCurrentlyInCreationException: Error creating bean with name 'A':
Requested bean is currently in creation: Is there an unresolvable circular reference?

典型场景:

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;   // A 依赖 B
}
 
@Service
public class ServiceB {
    @Autowired
    private ServiceA serviceA;   // B 依赖 A → 循环
}

Spring 三级缓存机制

Spring 通过三级缓存解决单例 + setter/field 注入的循环依赖(不适用于构造器注入):

级别Map 名称存储内容
一级缓存singletonObjects完整初始化好的 Bean
二级缓存earlySingletonObjects实例化但未完成属性注入的早期引用
三级缓存singletonFactoriesBean 工厂(用于生成早期引用,支持 AOP 代理)

解决过程(A 依赖 B,B 依赖 A):

1. 创建 A 的实例(构造器调用),放入三级缓存(ObjectFactory)
2. 注入 A 的属性 → 需要 B
3. 创建 B 的实例,放入三级缓存
4. 注入 B 的属性 → 需要 A
5. 从三级缓存获取 A 的 ObjectFactory,生成 A 的早期引用,放入二级缓存
6. B 完成属性注入,移入一级缓存
7. A 完成属性注入(B 已就绪),移入一级缓存

为什么构造器注入无法解决?

构造器注入时,Bean 实例化(调用构造器)本身就需要依赖对象,此时没有任何缓存可以提前暴露,必然死锁:

// 必然报错:构造器注入的循环依赖无法解决
@Service
public class ServiceA {
    private final ServiceB serviceB;
 
    public ServiceA(ServiceB serviceB) {  // 构造器注入
        this.serviceB = serviceB;
    }
}

Spring Boot 2.6+ 默认禁止循环依赖

Spring Boot 2.6 起,循环依赖默认报错。若临时需要允许(不推荐):

spring:
  main:
    allow-circular-references: true

这是一个逃生口,不应作为长期方案。


解决方案

方案一:重构设计(根本解决,首选)

循环依赖通常意味着职责划分不清,提取公共逻辑到第三个类:

// 原问题:OrderService ↔ UserService 互相依赖
@Service
public class OrderService {
    @Autowired UserService userService;
}
 
@Service
public class UserService {
    @Autowired OrderService orderService;
}
 
// 解决:提取共用逻辑到 OrderUserRelationService
@Service
public class OrderService {
    @Autowired OrderUserRelationService relationService;  // 无循环
}
 
@Service
public class UserService {
    @Autowired OrderUserRelationService relationService;  // 无循环
}
 
@Service
public class OrderUserRelationService {
    @Autowired OrderRepository orderRepo;
    @Autowired UserRepository userRepo;
    // 承载原来两个 Service 互相调用的逻辑
}

方案二:@Lazy(延迟注入,最快修复)

在其中一个注入点加 @Lazy,使该 Bean 在首次使用时才真正初始化,打破启动时的依赖环:

@Service
public class ServiceA {
 
    private final ServiceB serviceB;
 
    // @Lazy 让 Spring 注入一个代理对象,首次调用 serviceB 方法时才真正初始化
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Lazy 用于 Field 注入:

@Service
public class ServiceA {
 
    @Autowired
    @Lazy
    private ServiceB serviceB;
}

@Lazy 注入的是 CGLIB 代理,首次方法调用才触发真实 Bean 的初始化,几乎没有功能影响,但要注意代理不适用于 final 类。

延迟加载机制详见 延迟加载

方案三:setter / field 注入替代构造器注入

构造器注入的循环依赖无法解决,可改为 setter 注入打破环:

// 将其中一个改为 setter 注入
@Service
public class ServiceA {
 
    private ServiceB serviceB;
 
    // setter 注入允许在实例化后再注入,三级缓存可以介入
    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

注意:Spring 团队推荐构造器注入(便于测试、不可变),setter 注入仅作为打破循环的手段,而不是常规选择。

方案四:@PostConstruct 延迟获取

通过 @PostConstruct 在 Bean 完全初始化后再手动获取依赖:

@Service
public class ServiceA implements ApplicationContextAware {
 
    private ApplicationContext context;
    private ServiceB serviceB;   // 不在构造时注入
 
    @Override
    public void setApplicationContext(ApplicationContext ctx) {
        this.context = ctx;
    }
 
    @PostConstruct
    public void init() {
        // Bean 已完全就绪后再获取,避免循环
        this.serviceB = context.getBean(ServiceB.class);
    }
}

方案五:事件驱动解耦

将相互调用改为事件发布/订阅,彻底消除代码层面的依赖:

// ServiceA 发布事件,不再直接依赖 ServiceB
@Service
@RequiredArgsConstructor
public class ServiceA {
 
    private final ApplicationEventPublisher publisher;
 
    public void doSomething() {
        publisher.publishEvent(new SomethingHappenedEvent(this));
        // 不再需要 serviceB 字段
    }
}
 
// ServiceB 监听事件,不再直接依赖 ServiceA
@Service
public class ServiceB {
 
    @EventListener
    public void handle(SomethingHappenedEvent event) {
        // 处理逻辑
    }
}

事件机制详见 事件机制


特殊场景:@Configuration 类的循环依赖

@Configuration 类之间的 @Bean 方法互相依赖,Spring 通过 CGLIB 代理 @Configuration 类来保证 @Bean 方法的单例语义,通常可以自动处理:

@Configuration
public class ConfigA {
 
    @Bean
    public ServiceA serviceA(ServiceB serviceB) {  // 依赖 B
        return new ServiceA(serviceB);
    }
}
 
@Configuration
public class ConfigB {
 
    @Bean
    public ServiceB serviceB(ServiceA serviceA) {  // 依赖 A → 循环
        return new ServiceB(serviceA);
    }
}
// 此场景通常报错,需要重构或使用 @Lazy

AOP 代理与循环依赖

使用 @Transactional@Async、自定义切面时,Bean 会被 AOP 代理包装。三级缓存中的 ObjectFactory 正是为此设计——可以在早期引用阶段就生成代理对象,而非真实对象:

@Service
@Transactional           // AOP 代理
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}
 
@Service
public class ServiceB {
    @Autowired
    private ServiceA serviceA;  // 注入的是 ServiceA 的 AOP 代理,正常工作
}

如果循环依赖涉及代理且 Spring Boot 禁止了循环依赖,需要先解决循环,再加切面注解。

AOP 机制详见 AOP,事务代理详见 事务管理


诊断循环依赖

Spring Boot 启动失败时,错误信息会显示依赖链:

The dependencies of some of the beans in the application context form a cycle:

serviceA → serviceB → serviceA

排查步骤:

  1. 查看错误信息中的依赖链
  2. 找到环中”最不必要”的依赖关系
  3. 优先用设计重构(抽取公共类)消除
  4. 无法重构时,在一侧加 @Lazy

各方案对比

方案推荐度适用场景
重构设计(抽取公共类)★★★★★职责有明确重叠
事件驱动解耦★★★★☆单向通知场景
@Lazy★★★☆☆快速修复,短期过渡
setter 注入★★☆☆☆不适合构造器注入的遗留代码
allow-circular-references: true★☆☆☆☆仅用于迁移老项目的临时方案

相关链接

  • IOC与DI — Spring 依赖注入的三种方式(构造器/setter/field)
  • Bean生命周期 — Bean 实例化、属性注入、初始化的完整流程
  • 延迟加载@Lazy 的完整机制与应用场景
  • AOP — 代理对象的生成与三级缓存的关系
  • 事务管理@Transactional 代理与循环依赖
  • 事件机制 — 用事件解耦替代直接依赖