Bootstrap

Spring中的循环依赖问题

Spring的的循环依赖问题


一. 简介

1.什么是循环依赖问题?

类A依赖类B,类B也依赖类A,这种情况就会出现循环依赖。 Bean A → Bean B → Bean A

在这里插入图片描述

2.循环依赖有什么影响?

循环依赖会导致内存溢出

public class AService {
  private BService bService = new BService();
}

public class BService {
  private AService aService = new AService();
}

当你通过 new AService() 创建一个对象时你会获得一个栈溢出的错误。 如果你了解 Java的初始化顺序就应该知道为什么会出现这样的问题。

因为调用 new AService() 时会先去执行属性 bService 的初始化, 而 bService 的初始化又会去执行AService 的初始化, 这样就形成了一个循环调用,最终导致调用栈内存溢出。

二. 循环依赖复现

StudentA 依赖StudentB,同时 StudentB也依赖StudentA

@Component
public class StudentA {

    private String nameA;

    @Autowired
    private StudentB studentB;

    public StudentA( StudentB studentB) {
        this.studentB = studentB;
    }

}
@Component
public class StudentB {

    private String nameB;

    @Autowired
    private StudentA studentA;

    public StudentB( StudentA studengA) {
        this.studentA = studentA;
    }

}

启动工程,我们会看到如下报错,这就是循环依赖导致的程序运行问题

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-11-07 13:47:13.714 ERROR 12744 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  studentA defined in file [D:\prometheus\student-server\target\classes\com\student\studentserver\entity\xunhaunyilai\StudentA.class]
↑     ↓
|  studentB defined in file [D:\prometheus\student-server\target\classes\com\student\studentserver\entity\xunhaunyilai\StudentB.class]
└─────┘

三. 解决方案

1. 重新设计

当出现这种循环依赖时,很可能是在设计方面存在问题,没有把每个类的职责很好的区分开。应该尝试正确重新设计组件,以便其层次结构设计良好,并且不需要循环依赖项。‎如果无法重新设计,那么可以考虑其他解决办法。

2 使用 @Lazy

解决Spring循环依赖的一个简单方法就是对一个Bean使用延时加载。也就是说:这个Bean并没有完全的初始化完,实际上他注入的是一个代理,只有当他首次被使用的时候才会被完全的初始化。

@Component
public class StudentA {

    private String nameA;

    @Autowired
    private StudentB studentB;

    public StudentA(@Lazy StudentB studentB) {
        this.studentB = studentB;
    }

}
@Component
public class StudentA {

    private String nameA;

    @Autowired
    private StudentB studentB;

    public StudentA(@Lazy StudentB studengB) {
        this.studentB = studengB;
    }

}

加上@Lazy以后,启动工程后就不报错了

3. 使用setter注入
@Component
public class StudentA {


    @Autowired
    private StudentB studentB;

    public void testA(){
        System.out.println("student1");
    }
}
@Component
public class StudentB {

    @Autowired
    private StudentA studengA;

    public void testB(){
        System.out.println("studentB");
    }
}

这是一个经典的循环依赖,但是它能正常运行,得益于spring的内部机制,让我们根本无法感知它有问题,因为spring默默帮我们解决了(内部通过三级缓存进行解决)。

四. 三级缓存

三级缓存其实它更像是Spring容器工厂的内的术语,采用三级缓存模式来解决循环依赖问题,这三级缓存分别指

  • singletonObjects(一级缓存):用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用
  • earlySingletonObjects(二级缓存):提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖
  • singletonFactories(三级缓存):单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖(提前暴露)
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    ...
    // 从上至下 分表代表这“三级缓存”
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); //一级缓存
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存
    ...
    
    /** Names of beans that are currently in creation. */
    // 这个缓存也十分重要:它表示bean创建过程中都会在里面呆着~
    // 它在Bean开始创建时放值,创建完成时会将其移出~
    private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

    /** Names of beans that have already been created at least once. */
    // 当这个Bean被创建完成后,会标记为这个 注意:这里是set集合 不会重复
    // 至少被创建了一次的  都会放进这里~~~~
    private final Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));

先从一级缓存singletonObjects中去获取。(如果获取到就直接return)
如果获取不到或者对象正在创建中(isSingletonCurrentlyInCreation()),那就再从二级缓存earlySingletonObjects中获取。(如果获取到就直接return)
如果还是获取不到,且允许singletonFactories(allowEarlyReference=true)通过getObject()获取。就从三级缓存singletonFactory.getObject()获取。(如果获取到了就从singletonFactories中移除,并且放进earlySingletonObjects。其实也就是从三级缓存移动(是剪切、不是复制哦~)到了二级缓存)
加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决

getSingleton()从缓存里获取单例对象步骤分析可知,Spring解决循环依赖的诀窍:就在于singletonFactories这个三级缓存。这个Cache里面都是ObjectFactory,它是解决问题的关键。

流程梳理

  1. 实例化 A,此时 A 还未完成属性填充和初始化方法(@PostConstruct)的执行,A 只是一个半成品。
  2. 为 A 创建一个 Bean工厂,并放入到 singletonFactories 中。
  3. 发现 A 需要注入 B 对象,但是一级、二级、三级缓存均为发现对象 B。
  4. 实例化 B,此时 B 还未完成属性填充和初始化方法(@PostConstruct)的执行,B 只是一个半成品。
  5. 为 B 创建一个 Bean工厂,并放入到 singletonFactories 中。
  6. 发现 B 需要注入 A 对象,此时在一级、二级未发现对象A,但是在三级缓存中发现了对象 A,从三级缓存中得到对象 A,并将对象 A 放入二级缓存中,同时删除三级缓存中的对象 A。(注意,此时的 A还是一个半成品,并没有完成属性填充和执行初始化方法)
  7. 将对象 A 注入到对象 B 中。
  8. 对象 B 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 B。(此时对象 B 已经是一个成品)
  9. 对象 A 得到对象B,将对象 B 注入到对象 A 中。(对象 A 得到的是一个完整的对象 B)
  10. 对象 A完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 A

流程图总结:在这里插入图片描述

参考链接:
spring循环依赖与三级缓存
循环依赖及解决方法

五. 总结

整个从创建bean到解决循环依赖的过程:

context.getBean(A.class)->实例化->放入缓存->依赖注入B->getBean(B)->实例化B并放入缓存->B依赖注入A->getBean(A)获取到了缓存中的值并正常返回->B初始化成功->A初始化成功

;