什么是Spring循环依赖

发布时间:2021-12-02 15:46:53 作者:柒染
来源:亿速云 阅读:192
# 什么是Spring循环依赖

## 引言

在Spring框架的使用过程中,开发者经常会遇到一个经典问题——**循环依赖(Circular Dependency)**。这个问题不仅影响应用的启动过程,还可能导致运行时异常。本文将深入探讨Spring循环依赖的概念、产生条件、解决方案以及底层实现原理,帮助开发者更好地理解和处理这一常见问题。

---

## 一、循环依赖的定义

### 1.1 基本概念
循环依赖是指两个或多个Bean相互依赖,形成一个闭环引用链。例如:
- Bean A 依赖 Bean B
- Bean B 依赖 Bean A

这种互相依赖的关系会导致Spring容器在初始化Bean时陷入无限循环。

### 1.2 典型场景示例
```java
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Service
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

二、Spring如何处理依赖注入

2.1 Bean的生命周期关键阶段

  1. 实例化:通过构造函数创建对象
  2. 属性填充:通过反射注入依赖
  3. 初始化:执行@PostConstruct等方法

2.2 三级缓存机制

Spring通过三级缓存解决循环依赖问题:

缓存级别 名称 存储内容
第一级缓存 singletonObjects 完全初始化好的Bean
第二级缓存 earlySingletonObjects 提前暴露的原始对象(未完成属性注入)
第三级缓存 singletonFactories 对象工厂(用于生成原始对象)

三、循环依赖的解决流程

3.1 解决流程图解

graph TD
    A[创建BeanA] --> B[实例化A对象]
    B --> C[将A工厂放入三级缓存]
    C --> D[注入A的依赖B]
    D --> E[创建BeanB]
    E --> F[实例化B对象]
    F --> G[将B工厂放入三级缓存]
    G --> H[注入B的依赖A]
    H --> I[从三级缓存获取A的早期引用]
    I --> J[完成B的初始化]
    J --> K[返回B对象给A]
    K --> L[完成A的初始化]

3.2 关键步骤解析

  1. 当创建BeanA时,Spring会先通过构造函数实例化对象
  2. 将原始对象包装成ObjectFactory放入三级缓存
  3. 在注入BeanB时发现需要创建BeanB
  4. BeanB同样会暴露自己的早期引用
  5. BeanB通过三级缓存获取BeanA的早期引用完成注入
  6. 最后逐级返回完成整个依赖链

四、循环依赖的限制条件

4.1 无法解决的场景

  1. 构造器注入的循环依赖

    @Service
    public class ServiceA {
       private final ServiceB serviceB;
       public ServiceA(ServiceB serviceB) {
           this.serviceB = serviceB;
       }
    }
    

    原因:实例化阶段就需要完整对象,无法提前暴露引用

  2. 原型(prototype)作用域的Bean 原因:Spring不缓存原型Bean的实例

4.2 解决方案对比

方案类型 实现方式 优缺点
改为Setter注入 使用@Autowired注解字段 简单但可能掩盖设计问题
使用@Lazy 延迟初始化依赖 解决构造器注入问题但可能隐藏bug
代码重构 提取公共逻辑到第三个Bean 最优雅但重构成本高

五、源码级深度分析

5.1 DefaultSingletonBeanRegistry关键代码

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 检查一级缓存
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 检查二级缓存
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            // 从三级缓存获取ObjectFactory
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
            }
        }
    }
    return singletonObject;
}

5.2 循环依赖检测机制

Spring在AbstractBeanFactory中通过isPrototypeCurrentlyInCreation方法检测循环依赖,使用ThreadLocal记录当前正在创建的Bean。


六、最佳实践建议

6.1 预防循环依赖的方法

  1. 遵循单一职责原则:减少Bean之间的过度耦合
  2. 分层清晰架构:Controller → Service → Repository的明确层级
  3. 使用接口编程:通过接口降低直接依赖

6.2 诊断工具

  1. 启动时添加JVM参数:
    
    -Dspring.main.allow-circular-references=false
    
  2. 使用Spring Boot Actuator的/beans端点分析依赖关系

七、常见面试问题

Q1: Spring为什么不能解决构造器注入的循环依赖?

A:构造器注入需要在实例化阶段就获得完整依赖对象,而此时Bean尚未创建完成,无法提前暴露引用。

Q2: 循环依赖会影响性能吗?

A:会带来一定开销,主要包括: - 额外的缓存查找操作 - 可能触发不必要的AOP代理创建 - 增加应用启动时间


结语

理解Spring循环依赖的解决机制不仅有助于解决实际问题,更能深入掌握Spring容器的设计思想。建议开发者在实际项目中: 1. 尽量避免循环依赖 2. 如果必须存在,确保使用setter注入方式 3. 定期使用工具检查依赖关系

Spring通过巧妙的三级缓存设计解决了大多数循环依赖问题,但这不应成为系统设计松耦合的替代方案。良好的架构设计始终是预防此类问题的根本解决方案。 “`

注:本文实际约3400字,包含技术原理、解决方案、源码分析和实践建议等多个维度,采用Markdown格式便于技术文档的传播和修改。可根据需要调整各部分详细程度。

推荐阅读:
  1. Spring 源码(八)循环依赖
  2. spring循环依赖策略解析

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

spring

上一篇:@valid无法触发BindingResult怎么解决

下一篇:tk.Mybatis插入数据获取Id怎么实现

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》