Spring循环依赖产生与解决

 更新时间:2022年12月20日 10:46:43   作者:码畜c  
Spring的解决循环依赖是有前置条件的,要解决循环依赖我们首先要了解Spring Bean对象的创建过程和依赖注入的方式。依赖注入方式,我之前的博客有所分享,大家可以在看本篇文章之前进行一下小小的回顾

循环依赖产生情景

探讨如何解决循环依赖之前,更应该思考清楚什么情况下会发生这种问题?

1、模拟Prototype Bean的循环依赖

static class BeanA {
    // 1. 属性循环依赖
    BeanB beanB = new BeanB();
    // 2. 构造器循环依赖
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}
static class BeanB {
    // 1. 属性循环依赖
    BeanA beanA = new BeanA();
    // 2. 构造器循环依赖
    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }
}
public static void main(String[] args) {
    // 1. 属性循环依赖
    BeanA beanA = new BeanA(); // StackOverflowError
    // 2. 构造器循环依赖
    new BeanA(new BeanB(new BeanA(new BeanB( /*....*/ ))));
}

Prototype bean,构造器注入Bean时,强调注入的是成品Bean,无法解决循环依赖问题。

Prototype bean,属性上注入Bean时,由于原型模式是单个bean可以被多次创建的,一旦发生循环依赖,就会产生上面这种不断在创建新的BeanA与BeanB导致的栈溢出。源码中的解决方式:记录并检测,如下:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    // ...
    // Fail if we're already creating this bean instance:
    // We're assumably within a circular reference.
    if (isPrototypeCurrentlyInCreation(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
    // ...
    else if (mbd.isPrototype()) {
        // It's a prototype -> create a new instance.
        Object prototypeInstance = null;
        try {
            beforePrototypeCreation(beanName);
            prototypeInstance = createBean(beanName, mbd, args);
        }
        finally {
            afterPrototypeCreation(beanName);
        }
        bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
    }
    // ...
}
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
    Object curVal = this.prototypesCurrentlyInCreation.get();
    return (curVal != null &&
            (curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}
protected void beforePrototypeCreation(String beanName) {
    Object curVal = this.prototypesCurrentlyInCreation.get();
    if (curVal == null) {
        this.prototypesCurrentlyInCreation.set(beanName);
    }
    else if (curVal instanceof String) {
        Set<String> beanNameSet = new HashSet<>(2);
        beanNameSet.add((String) curVal);
        beanNameSet.add(beanName);
        this.prototypesCurrentlyInCreation.set(beanNameSet);
    }
    else {
        Set<String> beanNameSet = (Set<String>) curVal;
        beanNameSet.add(beanName);
    }
}

获取Bean前先判断是否当前Bean是否为Prototype并且在创建中。如果不是,且当前Bean为Prototype,那么会记录当前Bean正在创建中(通过beforePrototypeCreation方法)。

以模拟循环依赖代码为例:

  • 创建BeanA
  • populateBean(BeanA)
  • 创建BeanB
  • populateBean(BeanB)
  • 创建BeanA,isPrototypeCurrentlyInCreation发现BeanA正常创建中,抛出BeanCurrentlyInCreationException

2、模拟Singleton Bean的循环依赖

static class BeanA {
    BeanB beanB;
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
}
static class BeanB {
    BeanA beanA;
    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }
}
public static void main(String[] args) {
    // 1. 属性循环依赖
    BeanA beanA = new BeanA();
    BeanB beanB = new BeanB();
    beanA.beanB = beanB;
    beanB.beanA = beanA;
    // 2. 构造器循环依赖
    new BeanA(new BeanB(new BeanA(new BeanB( /*....*/ ))));
}

Singleton bean,构造器注入Bean时,强调注入的是成品Bean,无法解决循环依赖问题。

Singleton bean,属性上注入Bean时,允许注入提前暴露的半成品的Bean,即没有填充属性&初始化的Bean,只要引用关系能关联上,属性填充与初始化随着程序的执行自然就会处理完成,可以解决循环依赖。

为什么构造器中不能先传递一个半成品Bean,然后赋值给成员Bean属性呢?而一定要传递一个成品Bean?很好理解,spring并不能确定用户在构造器中的操作仅为赋值给Bean属性。且很多时候,我们需要通过bean的一些注入的Bean属性来执行一些业务操作的。如果为空,那么就可能会发生空指针异常。而且这样也不是特别合理。若用户选择属性注入的方式,用户不会涉及这些操作,那么自然就不需要考虑这些问题。

3、总结成一句话来说:spring仅能解决单例Bean下发生在属性上注入Bean产生的循环依赖。

Spring如何解决循环依赖

通过三级缓存解决循环依赖,分别是:

1、singletonObjects: 一级缓存,存储成品Bean

2、earlySingletonObjects: 二级缓存,存储半成品Bean

3、singletonFactories: 三级缓存,存储生成半成品Bean的ObjectFactory的lambda

还是以BeanA注入BeanB,BeanB注入BeanA为例,描述一下大致的执行流程:

  • 检查BeanA的缓存信息
  • 反射实例化BeanA
  • 注册暴露 BeanA的lambda(生成放入二级缓存中的半成品BeanA)到三级缓存:addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))
  • 填充属性BeanB:populateBean
  • 检查BeanB的缓存信息
  • 注册暴露 BeanB的lambda(生成放入二级缓存中的半成品BeanB)到三级缓存
  • 反射实例化BeanB
  • 填充属性BeanA:populateBean
  • 检查BeanA的缓存信息,从三级缓存中获取到了生成半成品BeanA的lambda,执行获取半成品BeanA并放入二级缓存,并在三级缓存中移除lambda
  • 将生成的BeanA的二级缓存对象赋值到BeanB的属性上
  • 将BeanB放入一级缓存,并在二、三级缓存中清理关于BeanB的记录
  • 初始化BeanB
  • 返回到第四步,将成品BeanB赋值到BeanA的属性上
  • 将BeanA放入一级缓存,并在二、三级缓存中清理关于BeanA的记录
  • 初始化BeanA

思考一下,是否能通过二级缓存来解决循环依赖?

首先spring对于三级缓存的应用,就是在生成需要提前暴露的半成品Bean,要么返回的是通过反射实例化后的对象,要么是被一组SmartInstantiationAwareBeanPostProcessor处理后的对象(比如生成代理Bean),然后在放到二级缓存中。那么我们在放入二级缓存时,不通过三级缓存获取这个过程,直接在方法中复现这个过程,在放入二级缓存中,效果想必也是相同的。

到此这篇关于Spring循环依赖产生与解决的文章就介绍到这了,更多相关Spring循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java中基本注解的知识点总结

    java中基本注解的知识点总结

    在本篇文章里小编给大家整理的是一篇关于java中基本注解的知识点总结,有需要的朋友们可以跟着学习下。
    2021-06-06
  • Java中的方法内联介绍

    Java中的方法内联介绍

    大家好,本篇文章主要讲的是Java中的方法内联介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-01-01
  • 如何利用Java正则表达式校验密码规则

    如何利用Java正则表达式校验密码规则

    正则表达式正则表达式是用来指定字符串模式的,可以方便的处理文本信息,这篇文章主要给大家介绍了关于如何利用Java正则表达式校验密码规则的相关资料,需要的朋友可以参考下
    2022-09-09
  • Springboot打成war包并在tomcat中运行的部署方法

    Springboot打成war包并在tomcat中运行的部署方法

    这篇文章主要介绍了Springboot打成war包并在tomcat中运行,在文中还给大家介绍了SpringBoot war包tomcat运行启动报错(Cannot determine embedded database driver class for database type NONE)的解决方法,需要的朋友可以参考下
    2018-01-01
  • Java类和对象的设计原理

    Java类和对象的设计原理

    这篇文章主要介绍了Java类和对象的设计原理,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的朋友可以参考一下
    2022-07-07
  • java控制台实现学生管理系统

    java控制台实现学生管理系统

    这篇文章主要为大家详细介绍了java控制台实现简单的学生管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02
  • 详解Java中雪花算法的实现

    详解Java中雪花算法的实现

    雪花算法是一种分布式的id生成算法。原理是将long分成若干个区段分别管理。本文将利用Java简单的实现雪花算法,感兴趣的可以了解一下
    2022-12-12
  • Spring Boot 中嵌入式 Servlet 容器自动配置原理解析

    Spring Boot 中嵌入式 Servlet 容器自动配置原理解析

    这篇文章主要介绍了Spring Boot 中嵌入式 Servlet 容器自动配置原理解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • 在spring中手写全局异常拦截器

    在spring中手写全局异常拦截器

    这篇文章主要介绍了如何在spring中手写全局异常拦截器,帮助大家更好的理解和使用spring框架,感兴趣的朋友可以了解下
    2020-11-11
  • Java简单实现对一串数字采用相应的加密策略后传输

    Java简单实现对一串数字采用相应的加密策略后传输

    下面小编就为大家带来一篇Java简单实现对一串数字采用相应的加密策略后传输。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-09-09

最新评论