🔄 「三级缓存魔法」揭秘Spring如何优雅解决循环依赖难题

181 阅读12分钟

🌱 引言

在Java开发中,Spring框架以其强大的依赖注入和控制反转功能深受开发者喜爱。然而,当我们的应用变得复杂时,Bean之间可能会出现相互依赖的情况,这就是所谓的"循环依赖"问题。😵 如果处理不当,循环依赖会导致应用启动失败,给开发者带来困扰。

幸运的是,Spring框架通过一种巧妙的机制——三级缓存,优雅地解决了大部分循环依赖问题。本文将深入探讨Spring是如何通过三级缓存机制处理循环依赖的,带你揭开这一技术奥秘的面纱。💡

阅读本文,你将收获:

•深入理解循环依赖的本质和危害

•掌握Spring三级缓存的工作原理

•通过源码分析了解Spring如何处理循环依赖

•学习不同场景下循环依赖的解决方案

•获取实际开发中的最佳实践建议

让我们开始这段探索Spring内部机制的奇妙旅程吧!🚀

🔄 什么是循环依赖?

循环依赖的基本概念

循环依赖是指两个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了一个环形调用。简单来说,就是A依赖B,B又依赖A,形成了一个闭环。🔄

Spring循环依赖示意图

如上图所示,Class A依赖Class B,同时Class B又依赖Class A,这就形成了最简单的循环依赖。在代码中,这种情况通常表现为:

@Component public class A { @Autowired private B b; }
@Component public class B { @Autowired private A a; }

为什么会出现循环依赖?

循环依赖通常与实际业务逻辑密切相关。在某些场景下,两个服务或组件确实需要相互协作,例如:

•用户服务需要调用订单服务获取用户的订单信息,而订单服务需要调用用户服务获取下单用户的详细信息

•父子关系的实体类,父实体需要知道所有子实体,子实体需要引用父实体

虽然从设计角度看,我们应该尽量避免循环依赖,但在某些业务场景下,循环依赖可能是不可避免的。😕

循环依赖的危害

如果没有适当的机制处理循环依赖,可能会导致以下问题:

启动失败:应用无法完成初始化过程

死锁:相互等待对方初始化完成,导致永远无法完成初始化

内存溢出:在递归创建过程中可能导致栈溢出

幸运的是,Spring框架为我们提供了解决循环依赖的机制,让我们来看看Spring是如何应对这一挑战的。👇

🧩 Spring三级缓存机制概述

什么是三级缓存?

Spring通过三级缓存的设计巧妙地解决了单例模式下的属性循环依赖问题。这三级缓存分别是:

Spring三级缓存结构图

1.一级缓存(singletonObjects):存放完全初始化好的Bean,即所谓的"成品"Bean

2.二级缓存(earlySingletonObjects):存放原始的Bean对象(尚未填充属性),即"半成品"Bean

3.三级缓存(singletonFactories):存放Bean工厂对象,用于生成原始Bean对象或其代理对象

这三级缓存的定义可以在Spring源码的DefaultSingletonBeanRegistry类中找到:

/** 一级缓存:存放完全初始化好的Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 二级缓存:存放原始的Bean对象(尚未填充属性) */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

/** 三级缓存:存放Bean工厂对象 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

各级缓存的作用

🔑 一级缓存(singletonObjects)

•作为单例池,存放所有已经完全初始化好的单例Bean

•当我们从容器中获取Bean时,首先会从一级缓存中获取

•所有单例Bean最终都会存放在这里

🔑 二级缓存(earlySingletonObjects)

•存放已实例化但尚未属性赋值、未执行初始化方法的Bean

•用于解决循环依赖,存放"半成品"Bean

•这是一个临时缓存,Bean最终会被移动到一级缓存

🔑 三级缓存(singletonFactories)

•存放Bean工厂对象(ObjectFactory)

•可以在需要时生成原始Bean对象或其代理对象(如果Bean被AOP切面代理)

•这也是一个临时缓存,获取对象后会从三级缓存移除,并放入二级缓存

为什么需要三级而非两级?

这是一个很多开发者都会思考的问题:既然二级缓存已经可以存放早期的Bean引用,为什么还需要三级缓存呢?🤔

核心原因在于AOP代理。在Spring中,如果一个Bean被AOP切面代理,那么最终注入的应该是代理对象而非原始对象。如果只有两级缓存:

1.在没有循环依赖的情况下,Spring会在Bean完全初始化后再创建代理对象

2.但在循环依赖的情况下,Bean还未完全初始化就需要被其他Bean引用

这就导致了一个问题:如果在循环依赖的情况下提前曝光原始对象,而最终需要的是代理对象,就会出现注入类型不一致的问题

三级缓存的设计巧妙地解决了这个问题:

•三级缓存中存放的是Bean工厂对象,可以在需要时才创建代理对象

•只有在真正发生循环依赖时,才会触发代理对象的创建

•这样既保证了循环依赖的解决,又确保了注入的一致性

🔍 源码深度解析

关键类和方法

要理解Spring如何处理循环依赖,我们需要关注以下关键类和方法:

•DefaultSingletonBeanRegistry:定义了三级缓存,实现了单例Bean的注册和获取逻辑

•AbstractAutowireCapableBeanFactory:负责创建Bean实例、填充属性和初始化Bean

•getSingleton():从缓存中获取单例Bean的方法

•doCreateBean():创建Bean实例的核心方法

•addSingletonFactory():将Bean工厂添加到三级缓存的方法

getSingleton方法分析

getSingleton()方法是解决循环依赖的关键,它定义了如何从三级缓存中获取Bean:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 首先从一级缓存获取
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果一级缓存没有,且当前Bean正在创建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 尝试从二级缓存获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 如果二级缓存没有,且允许早期引用
            if (singletonObject == null && allowEarlyReference) {
                // 尝试从三级缓存获取
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 通过工厂获取对象
                    singletonObject = singletonFactory.getObject();
                    // 放入二级缓存
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // 从三级缓存移除
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

这个方法的执行流程是:

1.先从一级缓存(singletonObjects)查找

2.如果没找到且Bean正在创建中,再从二级缓存(earlySingletonObjects)查找

3.如果还没找到且允许早期引用,则从三级缓存(singletonFactories)获取工厂对象

4.通过工厂对象创建Bean,放入二级缓存,并从三级缓存移除

添加到三级缓存的时机

在Bean创建过程中,Spring会在实例化后、属性填充前将Bean工厂添加到三级缓存:

// 添加到三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

这个getEarlyBeanReference方法非常关键,它可能会返回原始Bean对象,也可能返回代理对象:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                // 这里可能创建代理对象
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

💻 代码实战:不同场景下的循环依赖

场景一:setter注入的循环依赖(可解决)

Spring可以解决单例模式下通过setter方法注入的循环依赖:

@Component
public class A {
    private B b;
    
    public B getB() {
        return b;
    }
    
    @Autowired
    public void setB(B b) {
        this.b = b;
    }
}

@Component
public class B {
    private A a;
    
    public A getA() {
        return a;
    }
    
    @Autowired
    public void setA(A a) {
        this.a = a;
    }
}

这种情况下,Spring会:

1.完成A的实例化,并将其工厂添加到三级缓存

2.尝试填充A的属性,发现依赖B

3.开始创建B,完成实例化,并将其工厂添加到三级缓存

4.尝试填充B的属性,发现依赖A

5.此时可以从三级缓存中获取A的早期引用

6.完成B的初始化,并将B放入一级缓存

7.继续A的属性填充,注入B,完成A的初始化

8.将A放入一级缓存

场景二:构造器注入的循环依赖(无法解决)

Spring无法解决构造器注入的循环依赖

@Component
public class A {
    private B b;
    
    @Autowired
    public A(B b) {
        this.b = b;
    }
}

@Component
public class B {
    private A a;
    
    @Autowired
    public B(A a) {
        this.a = a;
    }
}

这种情况下会抛出BeanCurrentlyInCreationException异常,原因是:

•构造器注入是在实例化阶段就需要依赖对象

•而Spring的三级缓存机制是在实例化后才生效

•当创建A时,需要先有B的实例,但创建B又需要先有A的实例,形成死锁

场景三:原型作用域的循环依赖(无法解决)

Spring也无法解决原型(prototype)作用域的循环依赖

@Component
@Scope("prototype")
public class A {
    @Autowired
    private B b;
}

@Component
@Scope("prototype")
public class B {
    @Autowired
    private A a;
}

原因是:

•原型Bean每次获取都会创建新实例

•三级缓存机制只对单例Bean生效

•原型Bean不会被缓存,每次都是重新创建

场景四:AOP代理下的循环依赖处理

当Bean被AOP代理时,循环依赖的处理会更加复杂:

@Component
public class A {
    @Autowired
    private B b;
    
    @Transactional
    public void methodA() {
        System.out.println("A's method");
    }
}

@Component
public class B {
    @Autowired
    private A a;
    
    public void methodB() {
        a.methodA(); // 调用A的事务方法
    }
}

在这种情况下:

1.A因为有@Transactional注解,需要被AOP代理

2.当B依赖A时,注入的应该是A的代理对象而非原始对象

3.三级缓存中的工厂会在getEarlyBeanReference方法中创建代理对象

4.这确保了即使在循环依赖的情况下,B注入的也是A的代理对象

🔄 循环依赖解决流程图解

下面是Spring解决循环依赖的完整流程图,展示了从Bean创建到循环依赖解决的全过程:

Spring循环依赖解决流程图

这个流程与Bean的生命周期密切相关,循环依赖的解决发生在Bean的实例化和属性填充阶段:

Bean生命周期与循环依赖解决时机图

⚠️ 循环依赖的限制与陷阱

Spring无法解决的循环依赖场景

虽然Spring的三级缓存机制很强大,但仍有一些循环依赖场景是无法解决的:

1.构造器注入的循环依赖:如前所述,因为实例化阶段就需要依赖对象

2.原型作用域的循环依赖:因为原型Bean不会被缓存

3.使用@DependsOn导致的循环依赖:显式指定了初始化顺序

4.使用@Async的方法循环调用:可能导致死锁

常见错误与解决方案

当遇到Spring无法解决的循环依赖时,可以考虑以下解决方案:

1.将构造器注入改为setter注入:这是最简单有效的方法

2.使用@Lazy注解延迟依赖注入

3.重新设计避免循环依赖

•引入中间层打破循环

•合并有循环依赖的类

•使用事件机制替代直接依赖

🚀 实践建议与最佳实践

如何避免循环依赖

虽然Spring能够解决大部分循环依赖问题,但从设计角度看,避免循环依赖通常是更好的选择:

1.遵循单一职责原则:确保每个类只有一个职责,减少相互依赖

2.使用依赖倒置原则:依赖抽象而非具体实现

3.引入中间层:通过中间服务或事件机制解耦

4.使用观察者模式:替代直接的双向依赖

合理使用依赖注入

在实际开发中,可以遵循以下建议:

1.优先使用构造器注入:虽然无法解决循环依赖,但可以明确依赖关系,有助于发现潜在设计问题

2.必要时使用setter注入:在确实需要处理循环依赖时使用

3.慎用@DependsOn:避免创建显式的依赖顺序

4.合理规划Bean的作用域:尽量使用单例作用域

📝 总结与延伸

核心要点回顾

通过本文,我们深入了解了Spring处理循环依赖的机制:

1.Spring通过三级缓存机制解决单例Bean的循环依赖问题

2.三级缓存分别存放完成品Bean、半成品Bean和Bean工厂

3.三级缓存的设计主要是为了处理AOP代理对象的创建时机

4.Spring无法解决构造器注入和原型作用域的循环依赖

5.良好的设计应当尽量避免循环依赖

Spring设计哲学思考

Spring框架的三级缓存机制体现了其设计哲学:

开闭原则:通过扩展而非修改来适应新需求

单一职责:每个缓存有明确的职责

延迟加载:只在必要时才创建代理对象

优雅降级:尽可能解决问题,无法解决时给出明确错误

这种精心设计使Spring能够在保持简洁API的同时,内部处理复杂的依赖关系,为开发者提供便利。

相关知识推荐

如果你对Spring的内部机制感兴趣,可以进一步探索以下主题:

•Spring Bean的完整生命周期

•Spring AOP的实现原理

•Spring IoC容器的设计与实现

•Spring事务管理机制

通过深入理解这些核心机制,你将能够更加高效地使用Spring框架,并在遇到复杂问题时找到合适的解决方案。🌟

希望本文能帮助你理解Spring循环依赖的处理机制,如有疑问或建议,欢迎交流讨论!💬