Spring是如何解决循环依赖的?

86 阅读4分钟

引言

在软件开发过程中,循环依赖是一个常见且棘手的问题,特别是在面向对象的编程paradigm中。Spring框架作为Java生态系统中最广泛使用的依赖注入框架,提供了一套优雅而巧妙的机制来处理这类问题。本文将深入探讨Spring是如何解决循环依赖的,并通过详细的代码示例帮助读者深入理解其内部原理。

什么是循环依赖?

循环依赖是指在系统中,类A依赖于类B,同时类B又依赖于类A,形成一个闭环。举个具体的例子:

// ServiceA 依赖 ServiceB
public class ServiceA {
    private ServiceB serviceB;
    
    @Autowired
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

// ServiceB 依赖 ServiceA
public class ServiceB {
    private ServiceA serviceA;
    
    @Autowired
    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

在传统的依赖注入场景中,这种代码会导致应用程序启动失败,因为Spring无法完成对象的实例化。

Spring解决循环依赖的三级缓存机制

Spring主要通过"三级缓存"的方式来解决构造器注入和单例Bean的循环依赖问题。这是一个非常精妙的设计,涉及以下三个重要的缓存:

  1. 一级缓存(singletonObjects):存储完全初始化好的单例Bean实例。
  2. 二级缓存(earlySingletonObjects):存储提前暴露的Bean实例,但尚未完全初始化。
  3. 三级缓存(singletonFactories):存储Bean的ObjectFactory,用于创建Bean的早期引用。

源码解析:三级缓存的实现

让我们通过源码片段来理解这一机制:

// Spring DefaultSingletonBeanRegistry 部分源码
/** 一级缓存:存储完全初始化的Bean实例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 二级缓存:存储早期暴露的Bean实例 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

/** 三级缓存:存储Bean的ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

循环依赖解决流程详解

当Spring遇到循环依赖时,处理流程大致如下:

  1. 创建Bean的早期引用

    • 在对象A创建过程中,Spring会提前创建一个ObjectFactory并放入三级缓存
    • 此ObjectFactory可以获取Bean的早期引用,即未完全初始化的实例
  2. 依赖注入

    • 当对象B需要注入对象A时,会首先从缓存中获取A的引用
    • 如果一级缓存没有,则尝试从二级缓存获取
    • 如果二级缓存也没有,则通过三级缓存的ObjectFactory获取早期引用
  3. 属性填充与初始化

    • 最终完成两个对象的相互依赖注入
    • 将完全初始化的对象放入一级缓存

image.png

代码示例:循环依赖实践

@Component
public class CircularDependencyDemo {
    
    @Autowired
    private DependencyA dependencyA;
    
    @Autowired
    private DependencyB dependencyB;
}

@Component
public class DependencyA {
    @Autowired
    private DependencyB dependencyB;
    
    // 业务方法
    public void methodA() {
        System.out.println("DependencyA method executed");
    }
}

@Component
public class DependencyB {
    @Autowired
    private DependencyA dependencyA;
    
    // 业务方法
    public void methodB() {
        System.out.println("DependencyB method executed");
    }
}

注意事项与局限性

尽管Spring的三级缓存机制非常强大,但仍有一些使用限制:

  1. 构造器注入的循环依赖无法自动解决

    • Spring默认不支持构造器方式的循环依赖
    • 需要通过@Lazy注解或者重构依赖关系来解决
  2. 原型(Prototype)作用域的Bean不支持循环依赖

    • 仅对单例(Singleton)模式的Bean有效
  3. 复杂的依赖关系可能导致性能开销

    • 过多的循环依赖会增加内存消耗和系统复杂度

最佳实践建议

  1. 尽量避免循环依赖,这是一种不好的设计模式
  2. 如必须使用,优先考虑使用属性注入或Setter方法注入
  3. 重构代码,解耦组件间的紧密耦合
  4. 必要时可使用@Lazy注解延迟加载

结语

Spring的循环依赖解决方案体现了框架设计的巧妙之处。通过三级缓存机制,Spring优雅地处理了对象间复杂的依赖关系,为开发者提供了更加灵活的依赖注入方式。

理解这一机制不仅能帮助我们更好地使用Spring,还能启发我们在软件设计中如何思考和解决复杂的依赖问题。

公众号:trymoLiu

欢迎各位看官老爷~