• 从源码看Spring如何解决循环依赖的脉络?鸡生蛋与蛋生鸡的问题


    为什么会有循环依赖问题?

    当一个Chicken类引用Egg类作为属性依赖,同样Egg类中也引用依赖Chicken,那么最终就会成为一个闭环。如下图所示:

    这就好比鸡生蛋,蛋又生鸡,那么就会发生互相依赖又循环往复的关系就称为循环依赖

    循环依赖其实只是一个现象(场景),还不是一个问题。当你问ChickenEgg两个对象循环依赖后能否相互创建成功?这才是一个问题。

    例如下面代码所示,如果ChickenEgg使用构造器相互依赖注入,那么会陷入循环依赖注入的死循环。

    1. new Chickennew Eggnew Chicken(new Egg(...))))
    2. 复制代码

    我们再来看循环依赖问题在Spring中主要有的三种情况:

    • 构造器方式依赖注入
    • Set方式多例(原型)模式下依赖注入
    • Set方式单例依赖注入

    第一种构造方法注入的情况,在上面我们构造器方式的依赖注入会造成无限循环下去。第二种多例的情况,多例跟Spring默认创建单例相反,多例是每创建一次类,实例就会多一份。而单例是确保类的实例只允许一份;所以多例每创建一个Bean时,都会产生一个新的Bean,所以导致无限循环的产生Bean,那么必然导致OOM出现。

    在Spring中默认就是单例,所以Spring通过第三种set方式,可以实现循环依赖。代码如下所示:

    1. @Component
    2. public class Chicken {
    3. // Chicken中注入了Egg
    4.  @Autowired
    5.  private Egg egg;
    6. }
    7. 复制代码
    1. @Component
    2. public class Egg {
    3.  // Egg中也注入了Chicken
    4.  @Autowired
    5.  private Chicken chicken;
    6. }
    7. 复制代码

    解决循环依赖靠定义:三级缓存

    Spring为了解决单例的循环依赖问题,首先会定义三级缓存做准备(你会发现很多缓存都是用key-value的方式,Spring也如此,同样用了三个Map)。

    三个Map就定义在DefaultSingletonBeanRegistry类中。(现在可能对为什么要定义三级缓存感到陌生,但是别急,后面相信你会无比熟悉)

    1. public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    2. /** Cache of singleton objects: bean name to bean instance. */
    3. private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    4. /** Cache of singleton factories: bean name to ObjectFactory. */
    5. private final Map<String, ObjectFactory> singletonFactories = new HashMap<>(16);
    6. /** Cache of early singleton objects: bean name to bean instance. */
    7. private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    8. //省略其他
    9. }
    10. 复制代码

    深入源码看Spring解决循环依赖的脉络

    先来回顾一个Bena创建的主要流程:实例化Bean -> 填充Bean属性 -> 调用Bean初始化方法。

    如下图所示,(可以说是Bean生命周期制定的地图,

    Spring源码中在创建Bean的关键代码是使用getBean()方法。这也是创建Bean的开始。

    1. public Object getBean(String name) throws BeansException {
    2. return doGetBean(name, null, null, false);
    3. }
    4. 复制代码

    接着我们进到getBean()中可以看到doGetBean(),在Spring源码中带do前缀的方法才是真正干活的。如下代码所示:

    1. protected T doGetBean(final String name, @Nullable final Class requiredType,
    2. @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    3. final String beanName = transformedBeanName(name);
    4. Object bean;
    5. // 检查容器中是否已经有当前bean实例
    6. // Eagerly check singleton cache for manually registered singletons.
    7. Object sharedInstance = getSingleton(beanName);
    8. if (sharedInstance != null && args == null) {
    9. //省略日志记录
    10. bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    11. }
    12. //省略其他处理
    13. //创建Bean的实例对象
    14. // Create bean instance.
    15. if (mbd.isSingleton()) {
    16. sharedInstance = getSingleton(beanName, () -> {
    17. try {
    18. return createBean(beanName, mbd, args);
    19. }
    20. catch (BeansException ex) {
    21. // Explicitly remove instance from singleton cache: It might have been put there
    22. // eagerly by the creation process, to allow for circular reference resolution.
    23. // Also remove any beans that received a temporary reference to the bean.
    24. destroySingleton(beanName);
    25. throw ex;
    26. }
    27. });
    28. bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    29. }
    30. else if (mbd.isPrototype()) {
    31. // It's a prototype -> create a new instance.
    32. Object prototypeInstance = null;
    33. try {
    34. beforePrototypeCreation(beanName);
    35. prototypeInstance = createBean(beanName, mbd, args);
    36. }
    37. finally {
    38. afterPrototypeCreation(beanName);
    39. }
    40. bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
    41. }
    42. else {
    43. String scopeName = mbd.getScope();
    44. final Scope scope = this.scopes.get(scopeName);
    45. if (scope == null) {
    46. throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
    47. }
    48. try {
    49. Object scopedInstance = scope.get(beanName, () -> {
    50. beforePrototypeCreation(beanName);
    51. try {
    52. return createBean(beanName, mbd, args);
    53. }
    54. finally {
    55. afterPrototypeCreation(beanName);
    56. }
    57. });
    58. bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    59. }
    60. //省略其他处理
    61. return (T) bean;
    62. }
    63. 复制代码

    上述代码尽管很多,但是实际上doGetBean()方法主要两件事,如果从缓存中获取不到Bean,那么创建一个新的Bean。而一开始在缓存中是获取不到Bean的,所以会进行Bean的创建,在doGetBean中可以看到很醒目的createBean()方法。

    1. protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
    2. throws BeanCreationException {
    3. // 省略其他代码
    4. try {
    5. Object beanInstance = doCreateBean(beanName, mbdToUse, args);
    6. if (logger.isTraceEnabled()) {
    7. logger.trace("Finished creating instance of bean '" + beanName + "'");
    8. }
    9. return beanInstance;
    10. }
    11. // 省略其他catch异常处理
    12. }
    13. 复制代码

    createBean实际做事的还是,前缀带do的doCreateBean()来实际创建Bean的方法,主要来看两个核心动作。第一个动作是createBeanInstance()来创建Bean的实例(给Bean开辟新的内存空间)。

    1. createBeanInstance(beanName, mbd, args);
    2. 复制代码

    第二个动作是执行addSingletonFactory()方法,将Bean添加缓存。

    1. addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    2. 复制代码

    我们来看具体addSingletonFactory()方法,可以看到开始添加到第三级缓存。

    1. protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) {
    2. Assert.notNull(singletonFactory, "Singleton factory must not be null");
    3. synchronized (this.singletonObjects) {
    4. if (!this.singletonObjects.containsKey(beanName)) {
    5. this.singletonFactories.put(beanName, singletonFactory);
    6. this.earlySingletonObjects.remove(beanName);
    7. this.registeredSingletons.add(beanName);
    8. }
    9. }
    10. }
    11. 复制代码

    但会发现addSingletonFactory()中添加到三级缓存中是一个getEarlyBeanReference()方法,而执行到这个方法时还不是一个完整Bean,那么实际上三级缓存保存是提前暴露出的对象。

    在getEarlyBeanReference方法的上方是这样描述。(Spring开发者怕你看不懂源码,还写注释跟你说明白)

    Obtain a reference for early access to the specified bean 获取对指定bean的早期访问的引用。

    也是说getEarlyBeanReference()实际上只是直接将实例化创建的Bean返回了。(所以你可能会跟一颗剽悍的种子一样对三级缓存充满疑惑,图什么?别急,跟着一颗剽悍的种子去问个究竟)

    接着实例化完Bean,继续按照Bean的生命周期流程就走到了Bean的填充属性,所以会看到#doCreateBean->populateBean()方法。

    1. populateBean(beanName, mbd, instanceWrapper);
    2. 复制代码

    对Bean的填充属性是循环依赖源头的开始,因为此时意味着Chicken属性对Egg属性注入,而当Chicken依赖Egg,那么这个时候Spring又会去getBean(Egg)

    因为Egg需要注入Chicken,所以在创建Egg的时候,又会去调用getBean(Chicken),这个时候就又回到之前的流程了,但是不同的是,之前的getBean是为了创建Bean,而此时再调用因为上面Chicken在实例化后已经放到三级缓存,所以Egg可以从缓存中直接获取到Chicken

    所以此时Egg将从三级缓存中获取到的Chicken放入到二级缓存中,同时移除三级缓存中的工厂。接着Egg完成对Chicken的注入后,Egg可以初始化;当Egg初始化完成,那么就回到了Chicken完成对getBean(Egg)的注入,最后Chicken初始化。

    所以二级earlySingletonObjects所缓存的是完成实例化,但是还未进行属性注入及初始化的对象。

    在完成初始化后,就可以在#getSingleton->addSingleton()方法,将初始化的Bean放入到一级缓存,同时删除二级,三级缓存。

    1. protected void addSingleton(String beanName, Object singletonObject) {
    2. synchronized (this.singletonObjects) {
    3. // 添加到一级缓存
    4. this.singletonObjects.put(beanName, singletonObject);
    5. // 移除二级缓存
    6. this.singletonFactories.remove(beanName);
    7. // 移除三级缓存
    8. this.earlySingletonObjects.remove(beanName);
    9. // 将完成的BeanName添加到已经注册的单例集合
    10. this.registeredSingletons.add(beanName);
    11. }
    12. }
    13. 复制代码

    到此循环依赖完全打开。所以一级缓存中已经是完全创建好的单例Bean。

    为什么要用三级缓存:一、二级缓存能解决循环依赖吗?

    当再次回过头来,你会发现只用一、二级缓存似乎也可以解决缓存依赖,但是还存在如果循环依赖还使用Spring中的AOP代理时,此时没有三级缓存,就意味着二级缓存中刚实例化的Bean要完成AOP代理。

    有畅谈到,BeanPostProcessor后置处理器是AOP实现的关键。所以Bean在制定生命周期时,代理是放在最后一步来完成的,而不是在实例化后就立刻代理。

    所以没有三级缓存,只使用二级缓存,那么就破坏了Spring对Bean生命周期的制定。

    而通过三级缓存就可以很好的解决循环依赖后还有AOP代理问题。因为AOP代理有可能会把之前的原来的Bean换成代理Bean,所以导致Bean的版本不对,最终也会造成异常。

    所以最后,再来看一级、二级、三级缓存中分别存放的是什么状态的Bean时,是不是亲切多了呢?

    先是三级缓存为早期曝光对象工厂(singletonFactories),是lambada表达式。然后是二级缓存为早期曝光对象earlySingletonObjects,是完成实例化,但是还未进行属性注入及初始化的对象。最后是一级缓存为单例池(singletonObjects),是完全创建好的单例Bean。

    最后

    所以当我在梳理了Spring脉络后,再来看这些Spring中的各种细节时,就正如众里寻她千百度,蓦然回首,那细节却在灯火阑珊处。

    好了,夜深了,这些天熬的夜,也到这里了。

  • 相关阅读:
    Docker - 发送 Container 日志到 AWS CloudWatch
    mac上使用虚拟机vm, 里面的镜像挂起会占用电脑的内存吗, 挂起和关机的区别是什么, 会影响正常电脑的内存和硬盘使用吗
    聊聊接口设计的36个小技巧
    ESP8266-Arduino网络编程实例-ESP-Now-Many-to-One多设备通信
    iframe下使用el-input-number组件造成数据不同步
    CCPC 2021威海 - G. Shinyruo and KFC(组合数,小技巧)
    字符串常量池与StringBuilder
    Python库使用笔记—Dataframe
    关于 java 的动态绑定机制
    Linux内核基础 - list_splice_tail_init函数详解
  • 原文地址:https://blog.csdn.net/Trouvailless/article/details/126081823