剖析Spring 循环依赖

一、什么是 Spring 循环依赖?

1.1 定义与本质

循环依赖是指两个或多个 Spring Bean 之间存在相互引用的关系,形成一个依赖的闭环结构。这种依赖关系会导致 Bean 初始化过程中的"死锁"问题。具体表现为:

  • 简单循环依赖:Bean A 依赖 Bean B,同时 Bean B 又依赖 Bean A,形成 A ↔ B 的依赖环
  • 复杂循环依赖:Bean A 依赖 Bean B,Bean B 依赖 Bean C,而 Bean C 又依赖 Bean A,形成 A → B → C → A 的依赖链
  • 自环依赖:Bean A 直接或间接地依赖自身(较少见)

本质分析

  1. 初始化时序问题:Bean 的完整初始化需要其依赖项已经就绪,但依赖项同样需要等待当前 Bean 初始化完成
  2. 对象状态矛盾:Spring 需要完整构造对象实例,但循环依赖导致对象处于"半成品"状态
  3. 依赖解决机制:Spring 通过三级缓存(singletonFactories、earlySingletonObjects、singletonObjects)来打破这种循环

典型示例:

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;  // 依赖ServiceB
}

@Service 
public class ServiceB {
    @Autowired
    private ServiceA serviceA;  // 又依赖ServiceA
}

1.2 循环依赖的三种场景

Spring 对不同类型的循环依赖有不同的处理能力:

1.2.1 构造器注入循环依赖

  • 特征:通过构造函数参数相互注入
  • 示例
    @Service
    public class ServiceA {
        private final ServiceB serviceB;
        public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
    }
    
    @Service
    public class ServiceB {
        private final ServiceA serviceA; 
        public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
    }
    

  • Spring处理:直接抛出BeanCurrentlyInCreationException,因为构造函数必须立即获得依赖对象

1.2.2 字段/setter注入循环依赖

  • 特征:通过@Autowired字段或setter方法注入
  • 示例
    @Service
    public class ServiceA {
        @Autowired
        private ServiceB serviceB;
    }
    
    @Service
    public class ServiceB {
        @Autowired 
        private ServiceA serviceA;
    }
    

  • Spring处理流程
    • 开始创建 ServiceA(getSingleton 时)
    • 实例化 ServiceA(new 出来,半成品)
    • 将 ObjectFactory(用于创建 ServiceA 早期引用)放入三级缓存
    • 开始填充属性,发现需要 ServiceB
    • 创建 ServiceB(getSingleton)
    • 实例化 ServiceB
    • 将 ServiceB 的 ObjectFactory 放入三级缓存
    • 填充 ServiceB 属性时,发现需要 ServiceA
    • 查找 ServiceA,发现正在创建中,从三级缓存获取 ObjectFactory,调用 getObject() 获取 ServiceA 早期引用
    • 将 ServiceA 早期引用放入二级缓存,并从三级缓存移除
    • ServiceB 完成属性注入(拿到 ServiceA 早期引用),继续初始化,最终放入一级缓存
    • 回到 ServiceA,注入 ServiceB(已完全创建)
    • ServiceA 完成初始化,放入一级缓存

1.2.3 单例与原型Bean循环依赖

  • 特征:单例Bean依赖原型Bean,同时原型Bean又依赖该单例Bean
  • Spring处理:无法自动解决,因为原型Bean不参与缓存
  • 解决方案
    • 使用@Lazy延迟加载
    • 通过ApplicationContext.getBean()手动获取
    • 重构设计避免此类依赖

1.3 循环依赖的危害与影响

1.3.1 直接危害

  1. 启动失败:抛出BeanCurrentlyInCreationException,典型错误信息:
    Error creating bean with name 'serviceA': 
    Requested bean is currently in creation: Is there an unresolvable circular reference?
    

  2. 初始化阻塞:应用无法完成Spring容器初始化,导致服务不可用

1.3.2 潜在风险

  1. 内存泄漏

    • 在自定义BeanPostProcessor中错误处理可能导致对象重复创建
    • 某些缓存机制可能保留无效的Bean引用
  2. 性能问题

    • 复杂的循环依赖会增加容器启动时间
    • 运行时可能产生额外的代理对象
  3. 设计缺陷

    • 循环依赖往往是代码结构不合理的信号
    • 可能导致业务逻辑混乱和测试困难

1.3.3 实际案例

某电商系统曾因循环依赖导致:

  • 订单服务依赖库存服务
  • 库存服务依赖促销服务
  • 促销服务又依赖订单服务 结果:系统启动时需要额外5分钟解决依赖,且内存占用增加30%

最佳实践建议:

  1. 尽量避免循环依赖
  2. 如果必须使用,优先选择setter注入方式
  3. 对复杂依赖关系使用@Lazy注解
  4. 定期使用mvn dependency:tree分析依赖关系

二、Spring 如何解决循环依赖?核心原理:三级缓存

Spring 解决循环依赖的核心机制是三级缓存,通过提前暴露未初始化完成的 Bean 实例,打破依赖闭环。这种设计充分考虑了 Spring 框架中 Bean 生命周期的复杂性,以及 AOP 代理等高级特性的支持需求。下面我们先明确三级缓存的定义,再逐步分析其工作流程。

2.1 三级缓存的定义

在 Spring 的 DefaultSingletonBeanRegistry 类中,定义了三个核心的缓存集合,分别对应三级缓存:

// 一级缓存:存储完全初始化完成的单例Bean(可直接使用)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存:存储早期暴露的Bean实例(已实例化,但未完成属性注入和初始化方法调用)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存:存储Bean的工厂对象(用于生成早期Bean实例,支持AOP代理)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// 正在创建中的Bean名称集合(标记Bean是否处于创建过程中,避免重复创建和循环依赖检测)
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

缓存级别详解:

  1. 一级缓存(singletonObjects)

    • 存储的是完全初始化完成的单例Bean
    • 这些Bean已经完成了所有生命周期步骤:
      • 实例化(通过构造方法)
      • 属性注入(@Autowired、@Resource等)
      • 初始化方法调用(@PostConstruct)
      • 实现了InitializingBean接口的afterPropertiesSet()方法
    • 可以直接被应用程序使用
  2. 二级缓存(earlySingletonObjects)

    • 存储的是早期暴露的Bean实例
    • 这些Bean已经完成了实例化,但尚未完成:
      • 属性注入
      • 初始化方法调用
    • 主要用于解决循环依赖问题
  3. 三级缓存(singletonFactories)

    • 存储的是ObjectFactory对象
    • 这些工厂对象可以生成早期Bean实例
    • 关键区别:
      • 二级缓存存储的是确定的Bean实例
      • 三级缓存存储的是可以生成Bean实例的工厂
    • 支持AOP代理的动态生成

特殊集合说明:

  • singletonsCurrentlyInCreation:这是一个线程安全的Set,用于记录当前正在创建的Bean名称。它的主要作用是:
    • 防止同一个Bean被重复创建
    • 用于检测循环依赖
    • 配合Spring的循环依赖异常抛出机制

2.2 三级缓存的工作流程(以 A 依赖 B,B 依赖 A 为例)

让我们通过一个具体的场景来理解三级缓存的工作机制:

场景设定:

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

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

详细工作流程:

步骤 1:初始化 Bean A
  1. Spring容器启动,开始创建Bean A
  2. 将"serviceA"加入singletonsCurrentlyInCreation集合
  3. 调用ServiceA的构造方法,完成实例化
    • 此时ServiceA实例已经存在,但serviceB字段为null
  4. 创建ObjectFactory对象并存入三级缓存
    • 这个工厂的getObject()方法可以:
      • 返回原始ServiceA实例
      • 或者返回ServiceA的代理对象(如果需要AOP)
  5. 开始属性注入:
    • 发现需要注入ServiceB
    • 检查缓存:
      • 一级缓存:无ServiceB
      • 二级缓存:无ServiceB
      • 三级缓存:无ServiceB
  6. 暂停ServiceA的创建,开始创建ServiceB
步骤 2:初始化 Bean B
  1. 将"serviceB"加入singletonsCurrentlyInCreation集合
  2. 调用ServiceB的构造方法,完成实例化
  3. 创建ObjectFactory对象并存入三级缓存
  4. 开始属性注入:
    • 发现需要注入ServiceA
    • 检查缓存:
      • 一级缓存:无ServiceA
      • 二级缓存:无ServiceA
      • 三级缓存:找到ServiceA的ObjectFactory
  5. 调用ObjectFactory.getObject():
    • 如果ServiceA需要AOP代理,则生成代理对象
    • 否则返回原始ServiceA实例
  6. 将获取到的ServiceA实例(可能是代理)放入二级缓存
  7. 完成ServiceB的属性注入
  8. 执行ServiceB的初始化方法:
    • @PostConstruct方法
    • afterPropertiesSet()方法
  9. 将完整的ServiceB实例存入一级缓存
  10. 从二级和三级缓存中移除ServiceB相关数据
  11. singletonsCurrentlyInCreation移除"serviceB"
步骤 3:完成 Bean A 的初始化
  1. 继续ServiceA的属性注入:
    • 现在一级缓存中已有ServiceB
    • 注入完整的ServiceB实例
  2. 执行ServiceA的初始化方法
  3. 将完整的ServiceA实例存入一级缓存
  4. 从二级缓存中移除ServiceA的早期实例
  5. 从三级缓存中移除ServiceA的ObjectFactory
  6. singletonsCurrentlyInCreation移除"serviceA"

关键点总结:

  1. 缓存升级机制:Bean实例会随着其完成度的提高,从三级缓存逐步升级到一级缓存
  2. 早期暴露:在实例化后立即通过三级缓存暴露Bean,即使它还不完整
  3. 代理支持:三级缓存中的ObjectFactory可以智能处理AOP代理需求
  4. 线程安全:整个过程需要考虑并发情况,使用ConcurrentHashMap等线程安全集合

2.3 为什么需要三级缓存?二级缓存不够吗?

深入分析缓存层级必要性:

  1. 二级缓存的局限性

    • 假设只有二级缓存:
      • Bean A实例化后,直接将原始实例放入二级缓存
      • 当Bean B依赖Bean A时,获取到的是原始实例
      • 如果Bean A需要AOP代理,最终应该使用代理实例
      • 这就导致系统中存在两个Bean A实例:
        • 原始实例(被Bean B引用)
        • 代理实例(最终版本)
      • 破坏了单例原则,可能导致不可预期的行为
  2. 三级缓存的优势

    • 通过ObjectFactory延迟决定返回实例的类型:
      • 在真正需要时(getObject()调用时)才决定:
        • 是否需要创建代理
        • 是返回原始实例还是代理实例
    • 确保整个系统中:
      • 对于需要代理的Bean,所有依赖方都使用同一个代理实例
      • 对于普通Bean,所有依赖方都使用同一个原始实例
  3. AOP代理的特殊处理

    • 代理创建时机:
      • 传统情况:在Bean初始化完成后创建代理
      • 循环依赖情况:需要在早期就决定是否创建代理
    • AbstractAutowireCapableBeanFactory的getEarlyBeanReference方法:
      protected Object getEarlyBeanReference(String beanName, Object bean) {
          Object exposedObject = bean;
          if (!Boolean.FALSE.equals(this.earlyProxyReferences.get(beanName))) {
              exposedObject = resolveBeforeInstantiation(beanName, bean);
          }
          return exposedObject;
      }
      

    • 这个方法会:
      • 检查是否需要提前AOP代理
      • 如果需要,则创建并返回代理对象
      • 否则返回原始对象
  4. 性能考量

    • 三级缓存的设计实际上是一种懒加载策略:
      • 不是所有Bean都需要提前创建代理
      • 只有发生循环依赖时才会调用ObjectFactory
    • 减少了不必要的代理创建开销

结论: 三级缓存是Spring框架中一个精妙的设计,它:

  1. 完美解决了循环依赖问题
  2. 保持了单例原则
  3. 支持AOP代理等高级特性
  4. 兼顾了性能考量

三、Spring 无法解决的循环依赖场景深度解析

Spring 框架通过三级缓存机制(singletonFactories、earlySingletonObjects、singletonObjects)能够有效解决大多数单例Bean的循环依赖问题。然而,在某些特定场景下,循环依赖问题仍然无法自动解决。下面我们将详细分析这些特殊场景,探讨其背后的原理,并提供可行的解决方案。

3.1 构造器注入循环依赖

问题现象

当两个Bean通过构造器方式互相注入时,Spring容器会直接抛出BeanCurrentlyInCreationException,表明检测到了无法解决的循环依赖。

技术原理

Spring解决循环依赖的核心机制是提前暴露未完成初始化的Bean实例。但对于构造器注入:

  1. 构造器注入发生在Bean实例化阶段
  2. 必须首先获得所有构造参数才能创建实例
  3. 无法先创建不完整的实例再补充依赖

典型场景示例

@Component
public class OrderService {
    private final UserService userService;
    
    @Autowired
    public OrderService(UserService userService) {
        this.userService = userService;
    }
}

@Component
public class UserService {
    private final OrderService orderService;
    
    @Autowired
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
}

解决方案对比

方案实现方式适用场景缺点
字段注入使用@Autowired标注字段简单场景不利于单元测试
Setter注入使用@Autowired标注setter方法需要动态变更依赖代码稍显冗长
@Lazy注解在构造参数前加@Lazy必须保持构造器注入代理对象可能影响性能

最佳实践建议

对于必须使用构造器注入的场景,推荐组合使用@Lazy和构造器注入:

@Component
public class OrderService {
    private final UserService userService;
    
    @Autowired
    public OrderService(@Lazy UserService userService) {
        this.userService = userService;
    }
}

3.2 原型Bean循环依赖

问题本质

原型(Prototype)Bean的特殊性在于:

  1. 每次请求都创建新实例
  2. 不参与任何级别的缓存
  3. 完整的生命周期每次都会执行

问题复现流程

  1. 请求获取原型Bean A
  2. 开始创建A实例
  3. 发现需要注入原型Bean B
  4. 请求获取原型Bean B
  5. 开始创建B实例
  6. 发现需要注入原型Bean A
  7. 再次请求获取原型Bean A...
  8. 最终栈溢出或抛出异常

应用场景示例

@Component
@Scope("prototype")
public class PrototypeA {
    @Autowired
    private PrototypeB b;
}

@Component
@Scope("prototype")
public class PrototypeB {
    @Autowired
    private PrototypeA a;
}

解决方案实现

使用JSR-330的Provider接口:

@Component
@Scope("prototype")
public class PrototypeA {
    @Autowired
    private Provider<PrototypeB> bProvider;
    
    public void execute() {
        PrototypeB b = bProvider.get();
        // 使用b执行业务逻辑
    }
}

方案对比分析

方案优点缺点适用版本
Provider标准JSR-330方案需要手动调用get()Spring 4.3+
ObjectFactorySpring原生方案非标准接口全版本支持
方法注入自动处理仅适用于方法调用全版本支持

3.3 混合作用域循环依赖

问题特征

当作用域不同的Bean形成循环依赖链时,如:

  • 单例Bean A → 原型Bean B → 单例Bean A
  • 请求域Bean X → 单例Bean Y → 请求域Bean X

底层机制

  1. 单例Bean A开始初始化
  2. 需要注入原型Bean B
  3. 开始创建原型Bean B
  4. 需要注入单例Bean A
  5. 但单例Bean A尚未完成初始化
  6. 无法通过缓存获取完整实例

典型业务场景

  • 单例的缓存服务依赖原型的处理器
  • 原型的处理器又依赖缓存的单例服务
  • 形成跨作用域的循环引用

解决方案示例

使用ApplicationContextAware接口:

@Component
@Scope("prototype")
public class PrototypeProcessor implements ApplicationContextAware {
    private ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext context) {
        this.context = context;
    }
    
    public void process() {
        CacheService cacheService = context.getBean(CacheService.class);
        // 使用缓存服务
    }
}

设计建议

  1. 尽量避免跨作用域的Bean依赖
  2. 考虑使用事件驱动模型替代直接依赖
  3. 将共享逻辑提取到无状态组件中
  4. 使用门面模式封装复杂依赖关系

四、实际开发中如何排查与解决循环依赖?

4.1 循环依赖的排查方法

当 Spring 容器启动时抛出 BeanCurrentlyInCreationException,可以通过以下步骤排查循环依赖:

步骤 1:查看异常堆栈信息

异常堆栈中会明确提示 "循环依赖的 Bean 链条",例如:

org.springframework.beans.factory.BeanCurrentlyInCreationException: 
Requested bean is currently in creation: Is there an unresolvable circular reference?
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:296)
    ...
Caused by: org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'a': Injection of autowired dependencies failed;
    ...

通过堆栈信息,可以定位到正在创建的 Bean(如上述的 "a"),以及其依赖的 Bean。关键信息包括:

  1. 当前正在创建的 Bean 名称
  2. 依赖注入失败的具体位置
  3. 完整的调用链,展示 Bean 之间的依赖关系

步骤 2:分析 Bean 的依赖关系

可以通过以下方式梳理 Bean 之间的依赖关系:

  1. 代码静态分析

    • 查看代码中的注入注解:检查@Autowired@Resource@Inject等注解,明确每个 Bean 依赖哪些其他 Bean。
    • 使用 IDE 的 "Find Usages" 功能,查找某个 Bean 被哪些其他 Bean 引用。
  2. 运行时分析工具

    • 使用 Spring Boot Actuator:如果是 Spring Boot 项目,可以引入spring-boot-starter-actuator,通过/actuator/beans端点查看所有 Bean 的依赖关系(需要在配置文件中开启端点:management.endpoints.web.exposure.include=beans)。
    • 使用 Spring 自带的DefaultListableBeanFactory:通过getDependenciesForBean()方法获取指定 Bean 的所有依赖。
  3. 可视化工具

    • 使用 IDEA 插件:如 "Spring Assistant" 或 "Spring Beans",可以可视化展示 Bean 之间的依赖关系,快速定位循环依赖。
    • 使用 UML 工具绘制类图:通过绘制类之间的关系图,直观地发现循环引用。

步骤 3:确定循环依赖的类型

根据排查结果,判断循环依赖属于哪种类型:

  1. 构造器注入循环依赖

    • 两个 Bean 都通过构造器相互依赖
    • Spring 无法处理这种循环依赖,必须修改设计
  2. 属性注入循环依赖

    • 通过 setter 方法或字段注入的循环依赖
    • Spring 可以自动处理单例 Bean 的这种循环依赖
  3. 原型(Prototype)作用域循环依赖

    • 至少有一个 Bean 是原型作用域
    • 需要特殊处理
  4. 间接循环依赖

    • 通过多个 Bean 间接形成的循环依赖链
    • 需要分析整个依赖链条

4.2 常见的解决思路

1. 调整注入方式

  • 优先使用字段注入或 setter 注入
    • 避免使用构造器注入,尤其是在存在多依赖的场景下
    • Spring 对字段/setter 注入的循环依赖有内置解决方案

2. 延迟初始化

  • 使用@Lazy注解延迟初始化
    • 对于构造器注入的循环依赖,在依赖参数上添加@Lazy
    • 让 Spring 生成代理对象,推迟真实 Bean 的创建
    • 适用于不频繁使用的依赖

3. 使用工厂模式

  • 使用ProviderObjectFactory延迟获取依赖
    • 对于原型 Bean 或复杂依赖
    • 通过Provider.get()ObjectFactory.getObject()延迟获取 Bean
    • 避免在初始化时直接注入
    • 示例:
      @Service
      public class OrderService {
          private final Provider<UserService> userServiceProvider;
          
          public OrderService(Provider<UserService> userServiceProvider) {
              this.userServiceProvider = userServiceProvider;
          }
          
          public void processOrder() {
              UserService userService = userServiceProvider.get();
              // 使用userService
          }
      }
      

4. 重构设计

  • 拆分 Bean 的职责
    • 如果循环依赖是由于 Bean 的职责过于复杂导致的
    • 可以将 Bean 拆分为多个职责单一的 Bean
    • 从根本上消除循环依赖
    • 示例:
      • UserOrderService拆分为UserServiceOrderService
      • 提取公共逻辑到第三个服务中

5. 控制创建顺序

  • 使用@DependsOn指定 Bean 的创建顺序
    • 对于非循环依赖但创建顺序有要求的场景
    • 可以通过@DependsOn强制指定 Bean 的创建顺序
    • 注意:该注解无法解决循环依赖,仅用于调整创建顺序
    • 示例:
      @Service
      @DependsOn("databaseInitializer")
      public class UserService {
          // ...
      }
      

6. 使用接口解耦

  • 面向接口编程
    • 定义清晰的接口边界
    • 通过接口而非具体实现类来声明依赖
    • 降低耦合度

4.3 实战案例:解决构造器注入循环依赖

假设我们有以下循环依赖场景:

// 订单服务
@Service
public class OrderService {
    private final UserService userService;
    
    // 构造器注入用户服务
    public OrderService(UserService userService) {
        this.userService = userService;
    }
}

// 用户服务
@Service
public class UserService {
    private final OrderService orderService;
    
    // 构造器注入订单服务
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
}

解决方案 1:使用@Lazy注解

@Service
public class OrderService {
    private final UserService userService;
    
    // 在依赖参数上添加@Lazy注解,延迟UserService的初始化
    public OrderService(@Lazy UserService userService) {
        this.userService = userService;
    }
    
    // 业务方法(调用userService时才会触发真实实例的创建)
    public void createOrder(Long userId) {
        // 此时userService的真实实例才会被初始化
        String userName = userService.getUserName(userId);
        System.out.println("为用户【" + userName + "】创建订单");
    }
}

@Service
public class UserService {
    private final OrderService orderService;
    
    // 构造器注入OrderService(无需额外处理,因OrderService已通过@Lazy延迟依赖)
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
    
    // 业务方法
    public String getUserName(Long userId) {
        // 模拟从数据库获取用户名
        return "用户" + userId;
    }
}

案例说明

原理

  • @Lazy注解会让 Spring 为UserService生成一个动态代理对象
  • OrderService构造器注入时,注入的是代理对象而非真实实例
  • 只有当调用userService.getUserName()时,代理对象才会触发真实UserService实例的创建
  • 这打破了"创建 OrderService 需先有 UserService,创建 UserService 需先有 OrderService"的闭环

验证

  • 启动 Spring 容器后,调用orderService.createOrder(1L)
  • 控制台会正常输出"为用户【用户1】创建订单"
  • 无循环依赖异常

解决方案 2:使用 ApplicationContextAware

@Service
public class OrderService implements ApplicationContextAware {
    private ApplicationContext context;
    private UserService userService;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.context = applicationContext;
    }
    
    // 通过getter延迟获取UserService
    public UserService getUserService() {
        if (userService == null) {
            userService = context.getBean(UserService.class);
        }
        return userService;
    }
    
    public void createOrder(Long userId) {
        String userName = getUserService().getUserName(userId);
        System.out.println("为用户【" + userName + "】创建订单");
    }
}

@Service
public class UserService {
    private final OrderService orderService;
    
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
    
    public String getUserName(Long userId) {
        return "用户" + userId;
    }
}

优缺点

  • 优点:完全解除了构造器循环依赖
  • 缺点:引入了对Spring容器的依赖,代码不够优雅

最佳实践建议

  1. 预防胜于治疗

    • 在设计阶段就避免循环依赖
    • 遵循单一职责原则
    • 使用清晰的层次架构
  2. 监控与告警

    • 在CI/CD流程中加入循环依赖检查
    • 使用工具如ArchUnit进行架构测试
  3. 文档记录

    • 对必要的循环依赖进行文档说明
    • 解释采用当前解决方案的原因
  4. 定期重构

    • 定期审查代码中的循环依赖
    • 随着业务发展,可能有机会消除之前不得不保留的循环依赖

五、Spring 循环依赖的核心源码解析

5.1 核心入口:AbstractBeanFactory.getBean()

getBean()方法是Spring容器获取Bean实例的核心入口,整个Bean生命周期管理都从这里开始。其核心逻辑通过doGetBean()方法实现,具体步骤如下:

  1. Bean名称处理

    • 转换别名到规范名称(如处理"&"前缀的FactoryBean名称)
    • 示例:输入别名"myAlias"会被转换为注册的真实beanName
  2. 三级缓存查询

    • 优先检查完全初始化的单例
    • 如果未找到且该Bean正在创建中,会尝试获取早期引用
  3. 原型Bean校验

    if (isPrototypeCurrentlyInCreation(beanName)) {
        throw new BeanCreationException("原型Bean循环依赖检测失败");
    }
    

    • Spring不支持原型Bean的循环依赖,会立即抛出异常
  4. 父容器查询

    • 如果当前容器没有该Bean定义,会递归查询父容器
    • 典型应用场景:Spring MVC中DispatcherServlet容器会委托父容器查找Service层Bean
  5. 依赖处理

    • 解析@DependsOn注解声明的依赖关系
    • 确保依赖Bean先创建,如果检测到循环依赖会抛出异常
  6. 作用域处理

    • 单例(Singleton):通过getSingleton()方法管理
    • 原型(Prototype):每次调用都创建新实例
    • 自定义作用域:如Request/Session作用域,通过Scope接口实现

5.2 三级缓存查询逻辑:DefaultSingletonBeanRegistry.getSingleton()

Spring通过三级缓存机制解决循环依赖问题,具体实现如下:

缓存结构

  1. 一级缓存(singletonObjects)

    • 存储完全初始化好的单例Bean
    • ConcurrentHashMap实现,键为beanName,值为Bean实例
  2. 二级缓存(earlySingletonObjects)

    • 存储早期暴露的Bean实例(已实例化但未完成属性注入)
    • HashMap实现,需要同步控制
  3. 三级缓存(singletonFactories)

    • 存储ObjectFactory工厂对象
    • 可以生成早期代理对象(处理AOP场景)

查询流程示例

假设BeanA依赖BeanB,BeanB又依赖BeanA:

  1. 创建BeanA时,实例化后立即将ObjectFactory放入三级缓存
  2. 当BeanB需要注入BeanA时,会通过三级缓存获取早期引用
  3. 最终完成初始化后,BeanA会从三级缓存升级到一级缓存

线程安全实现

synchronized (this.singletonObjects) {
    // 双重检查锁定模式
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 查询二级缓存逻辑...
    }
}

5.3 早期实例暴露:AbstractAutowireCapableBeanFactory.doCreateBean()

doCreateBean()方法完成Bean实例的核心创建工作:

  1. 实例化阶段

    • 通过反射调用构造函数创建原始对象
    • 示例:instanceWrapper = createBeanInstance(beanName, mbd, args)
  2. 提前暴露引用

    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    

    • 将原始对象包装为ObjectFactory放入三级缓存
    • 处理AOP代理时会在这里生成早期代理对象
  3. 属性填充

    • 处理@Autowired等注解的依赖注入
    • 可能触发依赖Bean的创建,形成循环依赖
  4. 初始化阶段

    • 调用初始化方法(@PostConstruct等)
    • 执行AOP代理的最终生成
  5. 缓存升级

    • 完成所有初始化后,将Bean从三级缓存移至一级缓存
    • 清理早期引用相关缓存

典型时序图

创建BeanA -> 实例化 -> 暴露引用 -> 注入BeanB 
    -> 创建BeanB -> 实例化 -> 注入BeanA(从缓存获取早期引用)
    -> 完成BeanB初始化
-> 完成BeanA初始化

六、Spring 循环依赖的常见误区与最佳实践

6.1 常见误区

误区1:"禁用循环依赖后,所有循环依赖都会报错"

Spring框架提供了allowCircularReferences属性(自Spring Boot 2.6版本起默认禁用),用于控制是否允许循环依赖。但需要特别注意:

  1. 禁用后的实际影响范围

    • 仅会阻断"单例Bean字段注入循环依赖"的自动解决机制
    • 对于构造器注入的循环依赖、原型(prototype)作用域Bean的循环依赖,即使启用该属性也会报错
  2. 原因分析

    • 构造器注入:在Bean实例化阶段就需要完整依赖,无法通过三级缓存机制解决
    • 原型Bean:每次获取都会创建新实例,Spring无法保证其依赖关系的正确性

误区2:"三级缓存可以解决所有单例Bean循环依赖"

三级缓存机制虽然强大,但存在局限性:

  1. 典型问题场景

    @Component
    public class A {
        @Autowired
        private B b;
        
        @PostConstruct
        public void init() {
            b.doSomething(); // 危险操作!
        }
    }
    
    @Component
    public class B {
        @Autowired
        private A a;
        
        @Autowired
        private UserDao userDao; // 可能尚未注入
        
        public void doSomething() {
            userDao.query(); // 可能抛出NPE
        }
    }
    

  2. 根本原因

    • A的@PostConstruct方法在属性注入后立即执行
    • 此时B可能仍处于属性注入或初始化阶段
    • B的doSomething()方法依赖的userDao可能尚未完成注入
  3. 解决方案

    • 避免在初始化方法中调用依赖Bean的业务方法
    • 使用@Lazy延迟依赖Bean的初始化
    • 通过ApplicationListener在容器完全启动后执行逻辑

误区3:"使用@DependsOn可以解决循环依赖"

关于@DependsOn的正确认识:

  1. 实际作用

    • 仅强制指定Bean的创建顺序(如@DependsOn("b")表示A必须在B之后创建)
    • 无法解决循环依赖问题
  2. 限制场景

    @Component
    @DependsOn("b")
    public class A {
        @Autowired private B b;
    }
    
    @Component
    @DependsOn("a")
    public class B {
        @Autowired private A a;
    }
    

    • 这种互相@DependsOn的配置仍会抛出异常
  3. 适用场景

    • 非循环依赖但有创建顺序要求的情况
    • 例如:A需要使用B初始化后的某些资源

误区4:"循环依赖一定是代码设计问题,必须完全避免"

循环依赖的辩证看待:

  1. 不合理的情况

    • 服务层之间无节制的互相调用
    • 明显的职责划分不清
  2. 可能合理的情况

    • 订单服务与支付服务的双向依赖:
      • 订单创建需要调用支付接口
      • 支付结果回调需要更新订单状态
    • 领域模型中某些固有关系的表达
  3. 处理原则

    • 优先考虑通过设计消除不必要的循环
    • 对于必要的循环依赖,确保依赖调用的时机在Bean完全初始化后

6.2 最佳实践

1. 优先选择字段注入或Setter注入,谨慎使用构造器注入

注入方式对比

注入方式循环依赖支持代码简洁度适用场景
字段注入支持大多数场景
Setter注入支持可选依赖
构造器注入不支持无循环的必选依赖

构造器注入的特殊处理

@Component
public class A {
    private final B b;
    
    @Autowired
    public A(@Lazy B b) { // 必须配合@Lazy使用
        this.b = b;
    }
}

2. 合理使用@Lazy注解

使用要点

  • 作用目标:可以标注在字段、方法参数或配置类上
  • 工作原理:创建代理对象,延迟真实Bean的初始化
  • 注意事项:确保首次方法调用时Bean已完全初始化

典型应用

@Service
public class OrderService {
    @Lazy
    @Autowired
    private PaymentService paymentService; // 延迟初始化
}

3. 原型Bean依赖的特殊处理

解决方案对比

方案所属包特点
Providerjavax.inject标准JSR-330
ObjectFactoryorg.springframeworkSpring原生

实现示例

@Component
@Scope("prototype")
public class ReportGenerator {
    @Autowired
    private SingletonService singletonService;
}

@Component
public class SingletonService {
    @Autowired
    private Provider<ReportGenerator> reportGeneratorProvider;
    
    public void generateReport() {
        ReportGenerator generator = reportGeneratorProvider.get();
        generator.generate();
    }
}

4. 拆分复杂Bean

重构策略

  1. 职责分析

    • 识别Bean中可独立的功能模块
    • 评估依赖关系的合理性
  2. 事件驱动改造

    // 事件定义
    public class OrderPaidEvent {
        private Long orderId;
        private BigDecimal amount;
        // getters/setters
    }
    
    // 支付服务
    @Service
    public class PaymentService {
        @Autowired
        private ApplicationEventPublisher publisher;
        
        public void processPayment(Order order) {
            // 处理支付逻辑...
            publisher.publishEvent(new OrderPaidEvent(order.getId(), order.getAmount()));
        }
    }
    
    // 订单服务
    @Service
    public class OrderService {
        @EventListener
        public void handlePayment(OrderPaidEvent event) {
            // 更新订单状态
        }
    }
    

5. 安全的初始化时机控制

实现方案对比

方案优点缺点
@PostConstruct + CompletableFuture灵活控制代码稍复杂
ApplicationListener官方支持全局处理

示例代码

@Component
public class SystemInitializer implements ApplicationListener<ContextRefreshedEvent> {
    @Autowired
    private CacheService cacheService;
    
    @Autowired
    private ConfigService configService;
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 确保所有Bean已初始化完成
        cacheService.warmUp();
        configService.loadRemoteConfig();
    }
}

6. 循环依赖检测手段

全周期检测方案

  1. 开发阶段

    • IDEA安装Spring Assistant插件
    • 使用Dependency Analyzer分析依赖图
  2. 测试阶段

    @SpringBootTest
    class ContextLoadsTest {
        @Test
        void contextLoads() {
            // 如果存在未处理的循环依赖,测试将失败
        }
    }
    

  3. 生产环境

    • 启用Spring Boot Actuator
    • 定期检查/actuator/beans端点
    • 集成监控告警系统

依赖分析示例输出

+------------------+           +------------------+
|   OrderService   | <-------> |  PaymentService  |
+------------------+           +------------------+
      ^                                ^
      |                                |
      v                                v
+------------------+           +------------------+
|  UserService     |           |  AuditService    |
+------------------+           +------------------+

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值