hao同学的技术博客

  • 首页
  • Java
    • Java
    • JVM教程
    • Java面试
    • Java并发入门
    • Java并发进阶
  • 项目
    • 从零打造项目
  • Python
    • Python
    • Python爬虫
    • 算法
  • Java框架
    • Spring
    • SpringBoot
  • 前端
    • Angular
  • 其他
    • Linux
    • SQL
  • 随笔
分享技术,记录人生
一个痴迷于技术的厨艺爱好者
  1. 首页
  2. Spring
  3. 正文

Spring IoC之循环依赖处理

2022年5月27日 263点热度 0人点赞 0条评论

Spring IoC之循环依赖处理插图

什么是循环依赖

循环依赖其实是循环引用,也就是两个或则两个以上的 bean 互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图所示:

循环引用关系

注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。
Spring 中循环依赖场景有:

  • 构造器的循环依赖
  • field 属性的循环依赖

对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖,所以下面我们分析的都是基于 field 属性的循环依赖。

Spring 只解决 scope 为 singleton 的循环依赖,对于scope 为 prototype 的 bean Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。

如何检测循环依赖

检测循环依赖相对比较容易,Bean 在创建的时候可以给该 Bean 做标记,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。

解决循环依赖

我们先从加载 bean 最初始的方法 doGetBean() 开始,该方法位于 AbstractBeanFactory 类中。

在 doGetBean() 中,首先通过 transformedBeanName(name)获取 beanName,然后调用 DefaultSingletonBeanRegistry 类中 getSingleton()方法,该方法会根据 beanName 从单例 bean 缓存中获取,如果不为空则直接返回。

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
        synchronized(this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }

    return singletonObject;
}

这个方法主要是从三个缓存中获取,分别是:singletonObjects、earlySingletonObjects、singletonFactories,三者定义如下:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

它们就是 Spring 解决 singleton bean 的关键因素所在,被称为三级缓存,第一级为 singletonObjects,第二级为 earlySingletonObjects,第三级为 singletonFactories。这里我们可以通过 getSingleton() 看到他们是如何配合的,在分析该方法之前,提下其中的 isSingletonCurrentlyInCreation() 和 allowEarlyReference。

  • isSingletonCurrentlyInCreation():判断当前 singleton bean 是否处于创建中。即是否包含在 singletonsCurrentlyInCreation 中。bean 处于创建中也就是说 bean 在初始化但是没有完成初始化,有一个这样的过程其实和 Spring 解决 bean 循环依赖的理念相辅相成,因为 Spring 解决 singleton bean 的核心就在于提前曝光 bean。
  • allowEarlyReference:从字面意思上理解就是允许提前拿到引用。其实真正的意思是是否允许从 singletonFactories 缓存中通过 getObject()拿到对象,为什么会有这样一个字段呢?原因在于 singletonFactories 才是 Spring 解决 singleton bean 的诀窍所在,这个我们后续分析。

getSingleton() 整个过程如下: 首先从一级缓存 singletonObjects 中获取,如果没有且当前指定的 beanName 正在创建,就再从二级缓存中 earlySingletonObjects 获取,如果还是没有获取到且 allowEarlyReference 为 true,则从三级缓存 singletonFactories 中获取指定 beanName 对应的 singletonFactory,如果获取到则通过 singletonFactory# getObject() 获取对象,并将其加入到二级缓存 earlySingletonObjects 中,然后从三级缓存 singletonFactories 中删除获取到的 singletonFactory。如下:

singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);

这样就从三级缓存升级到二级缓存了。

上面是从缓存中获取,但是缓存中的数据从哪里添加进来的呢?继续查看 doGetBean()中的代码 一直往下跟会发现有这么一段代码:

if (mbd.isSingleton()) {
    sharedInstance = this.getSingleton(beanName, () -> {
        try {
            return this.createBean(beanName, mbd, args);
        } catch (BeansException var5) {
            this.destroySingleton(beanName);
            throw var5;
        }
    });
    bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

其中 createBean()就是用来创建 bean 的,其详细定义在 AbstractAutowireCapableBeanFactory 类中,我们主要为了查询缓存中的数据是怎么添加进来的,所以这里对于代码不进行过多的分析,后续章节会对其进行讲述。在 createBean()方法中有个 doCreateBean() 方法,当中有这么一段代码:

boolean earlySingletonExposure = mbd.isSingleton() && this.allowCircularReferences && this.isSingletonCurrentlyInCreation(beanName);
if (earlySingletonExposure) {
    if (this.logger.isTraceEnabled()) {
        this.logger.trace("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references");
    }

    this.addSingletonFactory(beanName, () -> {
        return this.getEarlyBeanReference(beanName, mbd, bean);
    });
}

如果 earlySingletonExposure == true 的话,则调用 addSingletonFactory() 将他们添加到缓存中,但是一个 bean 要具备如下条件才会添加至缓存中:

  • 单例
  • 运行提前暴露 bean
  • 当前 bean 正在创建中

其中 allowCircularReferences 属性用来设置是否在 bean 之间允许循环引用,并自动尝试解决它们,默认值为 true。

addSingletonFactory() 代码如下:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized(this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }

    }
}

从这段代码我们可以看出 singletonFactories 这个三级缓存才是解决 Spring Bean 循环依赖的诀窍所在。同时这段代码发生在 createBeanInstance() 方法之后,也就是说这个 bean 其实已经被创建出来了,但是它还不是很完美(没有进行属性填充和初始化),但是对于其他依赖它的对象而言已经足够了(可以根据对象引用定位到堆中对象),能够被认出来了,所以 Spring 在这个时候选择将该对象提前曝光出来让大家认识认识。

该段代码添加 singletonFactory 到三级缓存中时,且会删除二级缓存中 beanName 对应的内容,这个逻辑不是很清楚,在测试案例中进行调试的时候也没发现什么端倪,如果有大神了解这部分的逻辑,望不吝赐教。

介绍到这里我们发现三级缓存 singletonFactories 和 二级缓存 earlySingletonObjects 中的值都有出处了,那一级缓存在哪里设置的呢?在类 DefaultSingletonBeanRegistry 中可以发现这个 addSingleton() 方法,源码如下:

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized(this.singletonObjects) {
        this.singletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

添加至一级缓存,同时从二级、三级缓存中删除。这个方法在我们创建 bean 的链路中有哪个地方引用呢?在 doGetBean() 处理不同 scope 时,如果是 singleton,则调用 getSingleton(),如下:

if (mbd.isSingleton()) {
    sharedInstance = this.getSingleton(beanName, () -> {
        try {
            return this.createBean(beanName, mbd, args);
        } catch (BeansException var5) {
            this.destroySingleton(beanName);
            throw var5;
        }
    });
    bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            //....
            try {
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            //.....
            if (newSingleton) {
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

至此,Spring 关于 singleton bean 循环依赖已经分析完毕了。所以我们基本上可以确定 Spring 解决循环依赖的方案了:Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 缓存中),这样一旦下一个 bean 创建的时候需要依赖 bean ,则直接使用 ObjectFactory 的 getObject() 获取了,也就是 getSingleton()中的代码片段了。

实例分析

新建三个 bean 类,相互之间构成引用关系。

Person类

public class Person {

    private String name;
    private AbstractCar car;
    private String desc;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public AbstractCar getCar() {
        return car;
    }

    public void setCar(AbstractCar car) {
        this.car = car;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", 拥有一辆car=" + car +
                '}';
    }
}

AbstractCar 类

public class AbstractCar {

    private String brand ;
    private Money money;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public Money getMoney() {
        return money;
    }

    public void setMoney(Money money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "AbstractCar{" +
                "brand='" + brand + '\'' +
                ", money=" + money +
                '}';
    }
}

Money 类

public class Money {
    private String classification;
    private Person person;

    public String getClassification() {
        return classification;
    }

    public void setClassification(String classification) {
        this.classification = classification;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    @Override
    public String toString() {
        return "Money{" +
                "classification='" + classification + '\'' +
                ", person=" + person.getName() +
                '}';
    }
}

beans.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="car" class="com.msdn.bean.AbstractCar"  p:brand="宝马" p:money-ref="money" />

    <bean id="person" class="com.msdn.bean.Person" p:name="herish" p:car-ref="car" />

    <bean id="money" class="com.msdn.bean.Money" p:classification="工资" p:person-ref="person" />
</beans>

测试代码

@Test
public void cycleRely(){
    ClassPathResource resource = new ClassPathResource("config/beans.xml");
    BeanFactory beanFactory = new XmlBeanFactory(resource);

    Person person = (Person) beanFactory.getBean("person");
    System.out.println(person);
}

运行结果为:

Person{name='herish', 拥有一辆car=AbstractCar{brand='宝马', money=Money{classification='工资', person=herish}}}

分析

关于上述代码用图解的形式表示如下:

bean循环依赖

对测试代码进行调试,大概了解其中的逻辑跳转。首先是 beanFactory.getBean("person"),该方法最终会定位在 doGetBean("person"),执行 getSingleton(beanName, allowEarlyReference)返回结果为 null,继续向下执行,如果 scope 为 单例的情况下,执行 createBean(beanName, mbd, args)方法,具体实现在 AbstractAutowireCapableBeanFactory 类中,然后调用 doCreateBean(),在该方法中,createBeanInstance()方法会先初始化对象赋零值,然后在 addSingletonFactory()中将”person“添加到三级缓存中,接着在 populateBean()方法中给对象添加属性内容,该部分的关键代码为 applyPropertyValues()方法,当发现有一个 Car 的引用属性,然后在 BeanDefinitionValueResolver 类中的 resolveValueIfNecessary()方法内跳转到 resolveReference(),进而调用 doGetBean("car")方法。

同上述过程一致,进而调用 doGetBean("money")方法,因为 Monery 类中有对 Person 的引用,所以最后又会执行 doGetBean("person")方法。关于这点就像递归调用一样,我们假设最初的 doGetBean("person")为第一层,则最后的为第四层,我们接下来要做的就是不断返回到上一层。此时 beanFactory 关于缓存的结果为:

Spring代码调试

再次进入到 getSingleton(beanName, allowEarlyReference)方法时,二级缓存中会添加”person“,三级缓存并删除它。接着会执行 getObjectForBeanInstance()方法,然后返回 person 对象。在 DefaultSingletonBeanRegistry 类中的 registerDependentBean()方法会记录 bean 之间的引用关系,然后返回第三层,在第三层的 populateBean()执行完毕之后,最后 doCreateBean()方法会返回这样的结果:

Spring代码调试

在 createBean()执行结束后,接着会执行 getSingleton(beanName,singletonFactory),该方法会将 money 对象加入到一级缓存中。在返回 money 对象后,返回第二层。第二层返回结果为:

Spring代码调试

将 car 对象加入到一级缓存之后,来到了第一层。第一层返回结果为:

Spring代码调试

然后将 person 对象加入到一级缓存中,再把 singletonsCurrentlyInCreation 清空。最后得到的 beanFactory 结果为:

Spring代码调试

到这里,关于 Spring 解决 bean 循环依赖就已经分析完毕了。最后来描述下就上面那个循环依赖 Spring 解决的过程(A为person,B为car,C为money):首先 A 完成初始化第一步并将自己提前曝光出来(通过 ObjectFactory 将自己提前曝光),在初始化的时候,发现自己依赖对象 B,此时就会去尝试 get(B),这个时候发现 B 还没有被创建出来,然后 B 就走创建流程,在 B 初始化的时候,同样发现自己依赖 C,C 也没有被创建出来,这个时候 C 又开始初始化进程,但是在初始化的过程中发现自己依赖 A,于是尝试 get(A),这个时候由于 A 已经添加至缓存中(一般都是添加至三级缓存 singletonFactories ),通过 ObjectFactory 提前曝光,所以可以通过 ObjectFactory.getObject() 拿到 A 对象,C 拿到 A 对象后顺利完成初始化,然后将自己添加到一级缓存中,回到 B ,B 也可以拿到 C 对象,完成初始化,A 可以顺利拿到 B 完成初始化。到这里整个链路就已经完成了初始化过程了。

备注:测试案例也尝试了将 bean 之间的关系设置为依赖关系,但是执行结果报错。

参考文献

https://www.cnblogs.com/java-chen-hao/p/11139887.html

本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可
标签: Spring 源码解析
最后更新:2022年5月27日

hresh

这是一个专注于IT技术学习交流的个人技术博客网站,包括Java学习、Python爬虫、Web开发实践等领域,深耕Java领域,内容涵盖Java基础、Java并发编程、Java虚拟机、Java面试等核心知识点。

点赞
< 上一篇
下一篇 >

文章评论

取消回复

hresh

这是一个专注于IT技术学习交流的个人技术博客网站,包括Java学习、Python爬虫、Web开发实践等领域,深耕Java领域,内容涵盖Java基础、Java并发编程、Java虚拟机、Java面试等核心知识点。

文章目录
  • 什么是循环依赖
  • 如何检测循环依赖
  • 解决循环依赖
  • 实例分析
  • 参考文献
最新 热点 随机
最新 热点 随机
后端必知:遵循Google Java规范并引入checkstyle检查 Spring Security结合Redis实现缓存功能 Spring Security结合JWT实现认证与授权 Spring Security自定义认证逻辑实现图片验证码登录 Spring Security进阶学习 Spring Security入门学习
Java面试准备之集合系列二 Angular学习前期准备 关于即时编译器的优化措施 Spring Boot拦截器(Interceptor)详解 Scrapy框架之CrawlSpider 代理模式详解

COPYRIGHT © 2022 hao同学的技术博客. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

鄂ICP备2022007381号

鄂公网安备 42010302002449号