跳转到内容

Spring循环依赖

来自代码酷

Spring循环依赖[编辑 | 编辑源代码]

循环依赖(Circular Dependency)是Spring框架中一个常见的设计问题,当两个或多个Bean相互依赖时,就会形成循环依赖。例如,Bean A依赖Bean B,而Bean B又反过来依赖Bean A。Spring IoC容器通过特定的机制处理这种情况,但开发者仍需理解其原理以避免潜在问题。

什么是循环依赖?[编辑 | 编辑源代码]

循环依赖是指两个或多个组件(在Spring中通常是Bean)相互引用,形成一个闭环。例如:

  • Bean A → 依赖 → Bean B
  • Bean B → 依赖 → Bean A

这种依赖关系会导致Spring在初始化Bean时陷入无限循环,因此需要特殊的处理机制。

Spring如何处理循环依赖?[编辑 | 编辑源代码]

Spring通过三级缓存(三级对象存储)机制来解决循环依赖问题:

1. 一级缓存(singletonObjects):存储完全初始化好的Bean。 2. 二级缓存(earlySingletonObjects):存储提前暴露的Bean(尚未完成属性注入)。 3. 三级缓存(singletonFactories):存储Bean的工厂对象,用于生成半成品Bean。

Spring在创建Bean时,会先将其工厂对象放入三级缓存,然后在属性注入阶段发现循环依赖时,通过工厂对象提前暴露Bean的引用,从而打破循环。

处理流程示例[编辑 | 编辑源代码]

graph TD A[创建Bean A] --> B[实例化A, 放入三级缓存] B --> C[注入A的属性, 发现需要Bean B] C --> D[创建Bean B] D --> E[实例化B, 放入三级缓存] E --> F[注入B的属性, 发现需要Bean A] F --> G[从三级缓存获取A的工厂对象] G --> H[通过工厂获取A的早期引用, 放入二级缓存] H --> I[完成B的创建, 放入一级缓存] I --> J[完成A的创建, 放入一级缓存]

代码示例[编辑 | 编辑源代码]

以下是一个典型的循环依赖示例:

Bean定义[编辑 | 编辑源代码]

@Component
public class BeanA {
    @Autowired
    private BeanB beanB;
    
    public void doSomething() {
        System.out.println("BeanA使用BeanB");
        beanB.doSomething();
    }
}

@Component
public class BeanB {
    @Autowired
    private BeanA beanA;
    
    public void doSomething() {
        System.out.println("BeanB使用BeanA");
    }
}

测试代码[编辑 | 编辑源代码]

@SpringBootApplication
public class Application implements CommandLineRunner {
    @Autowired
    private BeanA beanA;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    @Override
    public void run(String... args) {
        beanA.doSomething();
    }
}

输出[编辑 | 编辑源代码]

BeanA使用BeanB
BeanB使用BeanA

循环依赖的限制[编辑 | 编辑源代码]

Spring只能解决单例作用域(Singleton)Bean的通过属性注入的循环依赖。以下情况无法解决:

1. 原型作用域(Prototype)Bean的循环依赖 2. 构造器注入导致的循环依赖 3. @PostConstruct方法中直接使用依赖对象

构造器注入导致的循环依赖示例[编辑 | 编辑源代码]

@Component
public class BeanC {
    private final BeanD beanD;
    
    @Autowired
    public BeanC(BeanD beanD) {
        this.beanD = beanD;
    }
}

@Component
public class BeanD {
    private final BeanC beanC;
    
    @Autowired
    public BeanD(BeanC beanC) {
        this.beanC = beanC;
    }
}

运行时会抛出BeanCurrentlyInCreationException异常。

最佳实践[编辑 | 编辑源代码]

1. 尽量避免循环依赖,重新设计代码结构 2. 如果必须使用循环依赖:

  * 使用setter注入而非构造器注入
  * 使用@Lazy注解延迟初始化

3. 对于必须使用构造器注入的场景,考虑使用ApplicationContext.getBean()手动获取

使用@Lazy解决构造器注入问题[编辑 | 编辑源代码]

@Component
public class BeanE {
    private final BeanF beanF;
    
    @Autowired
    public BeanE(@Lazy BeanF beanF) {
        this.beanF = beanF;
    }
}

数学表示[编辑 | 编辑源代码]

循环依赖可以表示为有向图中的环:

路径 ABA

其中A,B,...代表不同的Bean,箭头表示依赖关系。

实际应用场景[编辑 | 编辑源代码]

一个典型的实际案例是用户服务(UserService)和权限服务(PermissionService)的相互依赖:

  • UserService需要PermissionService来检查用户权限
  • PermissionService需要UserService来获取用户角色信息

解决方案通常是: 1. 提取公共逻辑到第三个服务 2. 使用接口分离 3. 应用上述的Spring解决方案

常见问题[编辑 | 编辑源代码]

为什么原型Bean不能解决循环依赖?[编辑 | 编辑源代码]

因为Spring不会缓存原型Bean的实例,每次请求都会创建新实例,无法通过提前暴露引用来解决循环依赖。

如何检测应用程序中的循环依赖?[编辑 | 编辑源代码]

1. 启动时观察BeanCurrentlyInCreationException 2. 使用Spring的CircularDependencyDetector工具类 3. 分析依赖关系图

总结[编辑 | 编辑源代码]

Spring通过三级缓存机制优雅地解决了大多数单例Bean的循环依赖问题,但开发者仍应: 1. 理解其工作原理 2. 知道其限制条件 3. 优先考虑代码重构而非依赖此机制 4. 在必须使用时选择合适的注入方式