首页 > 其他分享 >Spring 循环依赖解决方案

Spring 循环依赖解决方案

时间:2023-09-11 21:33:32浏览次数:43  
标签:依赖 Spring Bean clazz bean 缓存 解决方案

Spring解决循环依赖的思路与代码实现 (qq.com)

一文详解Spring Bean循环依赖 (qq.com)

Spring循环依赖解决方案 (qq.com)

结论

先说结论,Spring是通过三级缓存提前曝光的机制来解决循环依赖的问题。

两个Bean A,B互相引用循环依赖,Spring的解决过程如下:

  1. 通过构建函数创建A对象(A对象是半成品,还没注入属性和调用init方法)。
  2. A对象需要注入B对象,发现缓存里还没有B对象,将半成品对象A放入半成品缓存
  3. 通过构建函数创建B对象(B对象是半成品,还没注入属性和调用init方法)。
  4. B对象需要注入A对象,从半成品缓存里取到半成品对象A
  5. B对象继续注入其他属性和初始化,之后将完成品B对象放入完成品缓存
  6. A对象继续注入属性,从完成品缓存中取到完成品B对象并注入。
  7. A对象继续注入其他属性和初始化,之后将完成品A对象放入完成品缓存

三级缓存分别

三级缓存其实就是用三个Map来存储不同阶段Bean对象。

图片

一级缓存 singletonObjects: 主要存放的是已经完成实例化、属性填充和初始化所有步骤的单例Bean实例,这样的Bean能够直接提供给用户使用,我们称之为终态Bean或叫成熟Bean。

二级缓存 earlySingletonObjects: 主要存放的已经完成初始化但属性还没自动赋值的Bean,这些Bean还不能提供用户使用,只是用于提前暴露的Bean实例,我们把这样的Bean称之为临时Bean或早期的Bean(半成品Bean)

三级缓存 singletonFactories: 存放的是ObjectFactory的匿名内部类实例,调用ObjectFactory.getObject()最终会调用getEarlyBeanReference方法,该方法可以获取提前暴露的单例bean引用。

什么是循环依赖

首先说明一下,本文讨论的循环依赖仅针对scope为singleton,且非构造函数注入的bean。如果是prototype的bean,或者使用的是构造函数注入,Spring会直接抛出BeanCurrentlyInCreationException异常,并不会去通过什么手段解决这些循环依赖。

我们在使用Spring的过程中,会有很多bean依赖注入的场景。因为没有严格的规范约束,我们在使用的过程中,比较容易就会产生beanA依赖beanB,而beanB又依赖beanA的情况。

image-20230911201805309

这时,我们在创建beanA的输入,发现要注入beanB,就去尝试创建beanB,又发现要注入beanA,又要去创建beanA。至此,我们发现创建beanA依赖创建beanA,形成了死循环。

image-20230911201840974

解决思路

其实要打破上述这个循环的链条,关键点在于,将bean实例化和bean属性注入这2步分开,且允许在属性注入的时候,注入一个已经实例化但还未进行属性注入的bean。即让一个已经实例化的bean,提前暴露出来,可以被其他bean拿到引用进行属性注入,而这个提前暴露的bean的属性输入可以在后续过程中再完成,因为我们的目标bean在进行属性注入的时候,只要拿到这个提前暴露bean的引用即可。

这个思路也跟上面说的不支持构造方法输入的bean循环依赖是呼应的,因为实例化这一步就使用到了构造方法,如果是构造方法注入,这个bean都无法实例化出来,就没有可能进行提前暴露了。顺着这个思路,我们很自然可以想到使用一个map来保存那些实例化之后的bean,这个bean可能仅仅是实例化,还未进行属性注入,其他bean如果依赖它,就可以从这map中获取到并进行注入。

image-20230911202012112

示例代码

根据上面的思路,我们尝试使用代码进行实现。核心是为了说明如何解决循环依赖,我们对其他部分做了一定的简化:定义2个类,BeanA和BeanB,BeanA中有个BeanB类型的属性b,BeanB中有个BeanA类型的属性a;

我们的目标是使用上面解决循环依赖的思路,构造出2个对象,且对象互相持有对方的引用。

我们先定义2个bean类,各自持有对方类型的一个属性:

@Data
public class BeanA {

    private BeanB b;
}
@Data
public class BeanB {

    private BeanA a;
}

定义一个CycleDependency类,在main方法中模拟bean加载的过程,构造出2个对象:

import com.itheima.bean.BeanA;
import com.itheima.bean.BeanB;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class CycleDependency {

    private static final Map<Class<?>, Object> map = new HashMap<>(2);

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 模拟获取到需要加载的bean
        Class<?>[] classes = new Class[]{BeanA.class, BeanB.class};
        // 遍历列表加载bean
        for (Class<?> item: classes) {
            getBean(item);
        }
        // 断言校验记载到bean的属性
        assert Objects.requireNonNull(getBean(BeanA.class)).getB() == getBean(BeanB.class);
        assert Objects.requireNonNull(getBean(BeanB.class)).getA() == getBean(BeanA.class);
    }

    private static <T> T getBean(Class<T> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 查看缓存中是否存在
        if (map.containsKey(clazz)) {
            if (clazz.isInstance(map.get(clazz))) {
                return clazz.cast(map.get(clazz));
            }
            return null;
        }
        // 通过构造方法实例化bean
        Object object = clazz.getDeclaredConstructor().newInstance();
        // 将构造出来的bean放入缓存,提前暴露引用;注意,这里的bean还没有做属性注入
        map.put(clazz, object);
        // 模拟属性注入
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            Class<?> fieldType = field.getType();
            field.set(object, map.getOrDefault(fieldType, getBean(fieldType)));
        }
        // 返回构造出来的bean
        if (clazz.isInstance(object)) {
            return clazz.cast(object);
        }
        return null;
    }
}

代码解读

主要解读一下CycleDependency这个类,总共代码行数也不到50,我们直接从上往下解读,为了方便,我进行了截图和分块。

图片

最上面,定义了一个map,为了方便说明,我这里key使用的是Class(Spring的三级缓存都是使用String,bean的名称),value就是bean的实例对象。

在main方法中,我们分成了3块:

  1. 模拟获取到需要加载的bean,这里就直接是BeanA和BeanB的class数组
  2. 遍历步骤1的列表,调用getBean()方法去加载bean
  3. 对于获取到的bean,校验是否相互持有对方的引用

另外,对于第3点assert,需要在idea开启vm参数-ea才会生效;如果实在不生效,可以直接打印出是否相等的结果在控制台进行查看。

核心的代码其实是这个getBean()方法,我们接下来看下这个方法

图片

  1. 查看在map缓存中是否已经存在了,如果存在了就直接返回
  2. 调用构造方法实例化bean
  3. 将构造出来的对象放到缓存map中进行提前暴露,注意,这里的bean还没有进行属性注入
  4. 利用反射获取bean的属性,利用field.set模拟属性注入;因为我们一直知道属性只能是BeanA或者BeanB,这里也尝试先从缓存获取,如果获取不到就调用getBean()方法递归获取
  5. 返回构造好的对象,这个对象已经进行了属性注入了

进一步思考

上面我们利用一个map做缓存,模拟了一下最简易的处理循环依赖的情况。可以看到,我们只有一级缓存map,就解决了循环依赖,那么Spring为什么要使用三级缓存来处理循环依赖呢?

为什么有第2级

细心的你一定已经发现了,上面的缓存map存在一个问题,就是存放到这个map中的bean,并不保证已经完全可用了,我们在实例化之后,属性注入之前,就为了提前暴露,把bean对象存放到这个map中。

而Spring肯定需要另外一级缓存,只存在已经完全可用的bean。所以,我们可以对上面代码做一下改造,新定义一个map变量singletonObjects,存放已经完全可用的bean,我们原始代码中的map,作为第2级缓存使用。

简单修改上述代码,增加1级缓存,现在我们使用了2级缓存来解决存换依赖,同时还保证了在1级缓存singletonObjects中的bean都是属性注入后的bean。

public class CycleDependency {

    // 一级缓存,存放完全可用的bean
    private static final Map<Class<?>, Object> singletonObjects = new HashMap<>(4);

    // 二级缓存,存放的bean可能还未进行属性注入
    private static final Map<Class<?>, Object> map = new HashMap<>(4);

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 模拟获取到需要加载的bean
        Class<?>[] classes = new Class[]{BeanA.class, BeanB.class};
        // 遍历列表加载bean
        for (Class<?> item: classes) {
            getBean(item);
        }
        // 断言校验记载到bean的属性
        assert Objects.requireNonNull(getBean(BeanA.class)).getB() == getBean(BeanB.class);
        assert Objects.requireNonNull(getBean(BeanB.class)).getA() == getBean(BeanA.class);
    }

    private static <T> T getBean(Class<T> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 查看一级缓存中是否存在
        if (singletonObjects.containsKey(clazz)) {
            if (clazz.isInstance(singletonObjects.get(clazz))) {
                return clazz.cast(singletonObjects.get(clazz));
            }
            return null;
        }
        // 查看二级缓存中是否存在
        if (map.containsKey(clazz)) {
            if (clazz.isInstance(map.get(clazz))) {
                return clazz.cast(map.get(clazz));
            }
            return null;
        }
        // 通过构造方法实例化bean
        Object object = clazz.getDeclaredConstructor().newInstance();
        // 将构造出来的bean放入缓存,提前暴露引用;注意,这里的bean还没有做属性注入
        map.put(clazz, object);
        // 模拟属性注入
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            Class<?> fieldType = field.getType();
            field.set(object, map.getOrDefault(fieldType, getBean(fieldType)));
        }
        // 属性注入后,放入一级缓存
        singletonObjects.put(clazz, object);
        // 返回构造出来的bean
        if (clazz.isInstance(object)) {
            return clazz.cast(object);
        }
        return null;
    }
}

为什么有第3级

为什么要包装一层ObjectFactory对象存入三级缓存,说是为了解决Bean对象存在aop代理情况,那么直接生成代理对象半成品Bean放入二级缓存中,这样就可以不用三级缓存了!!!这么一说使用三级缓存的意义在哪里

首先需要明确一点:正常情况下(没有循环依赖),Spring都是在创建好完成品Bean之后才创建对应的代理对象。为了处理循环依赖,Spring有两种选择:

  1. 不管有没有循环依赖,都提前创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入。
  2. 不提前创建好代理对象,在出现循环依赖被其他对象注入时,才实时生成代理对象。这样在没有循环依赖的情况下,Bean就可以按着Spring设计原则的步骤来创建。

显然Spring使用了三级缓存,选择第二种方案,这是为啥呢?

原因是:如果要使用二级缓存解决循环依赖,意味着Bean在构造完后就创建代理对象,这样违背了Spring设计原则。Spring结合AOP跟Bean的生命周期,是在Bean创建完全之后通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。

Spring解决不了的循环依赖

我们在文章开头提到过:本文讨论的循环依赖仅针对scope为singleton,且非构造函数注入的bean。

  1. 为什么“非构造函数注入”,应该已经解释过了,因为如果是构造函数注入,无法进行实例化这一步,更不用说提前暴露了。但是还未没有说明为什么“scope为singleton”。
  2. 与Spring中默认的scope=singleton对象的,还有1种scope=prototype,从上面的解决存换依赖的思路可知,我们使用了一个map来缓存提前暴露的对象,所以,我们在目标bean属性注入的时候,从map中拿到的是同一个beanA的对象,如果这个scope=prototype,意味着,我们这里需要新建一个bean,不能使用缓存中的bean,所以上面的思路是无法解决的多例bean的循环依赖的。

总结

总的来说,Spring 解决循环依赖把握住两个关键点:

  • 提前暴露:刚刚创建好的对象还没有进行任何赋值的时候,将之暴露出来放到缓存中,供其他 Bean 提前引用(二级缓存)。
  • 提前 AOP:A 依赖 B 的时候,去检查是否发生了循环依赖(检查的方式就是将正在创建的 A 标记出来,然后 B 需要 A,B 去创建 A 的时候,发现 A 正在创建,就说明发生了循环依赖),如果发生了循环依赖,就提前进行 AOP 处理,处理完成后再使用(三级缓存)。

原本 AOP 这个过程是属性赋完值之后,再由各种后置处理器去处理 AOP 的(AbstractAutoProxyCreator),但是如果发生了循环依赖,就先 AOP,然后属性赋值,最后等到后置处理器执行的时候,就不再做 AOP 的处理了。

如果没有三级缓存

如果没有三级存储, Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有Bean都需要在实例化完成之后就立马判断是否需要生成代理对象。而Spring的设计原则是在Bean初始化完成之后才为其创建代理。

Spring Boot高版本循环依赖报错问题

Spring Boot2.6之后的版本,默认把循环依赖给禁用了。如果项目中存在循环依赖,需要添加一行配置

spring.main.allow-circular-references=true

否则会启动报错,说明Spring是不太赞成我们用循环依赖的。当有循环依赖产生的时候,这时候我们需要检查一下我们设计可能出了问题。

说说BeanFactory、FactoryBean及ObjectFactory三者的作用和区别?

BeanFactory: BeanFactory是IOC容器的核心接口,用于管理Bean的一个工厂接口类,主要功能有实例化、定位、配置应用程序中的对象及建立这些对象间的依赖

FactoryBean: 一般情况下,Spring 通过反射机制利用 bean 的 class 属性指定实现类来实例化 bean 。某些情况下,实例化 bean 过程比较复杂,如果按照传统的方式,则需要提供大量的配置信息,配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring 为此提供了一个 FactoryBean 的工厂类接口,用户可以通过实现该接口定制实例化 bean 的逻辑。FactoryBean在BeanFacotry的实现中有着特殊的处理,如果一个对象实现了FactoryBean 那么通过它get出来的对象实际是 factoryBean.getObject() 得到的对象,如果想得到FactoryBean必须通过在 '&' + beanName 的方式获取。

ObjectFactory: ObjectFactory则只是一个普通的对象工厂接口,从上面可以看到spring对ObjectFactory的应用之一就是,将创建对象 的步骤封装到ObjectFactory中,从而通过ObjectFactory在合适的时机创建合适的bean

标签:依赖,Spring,Bean,clazz,bean,缓存,解决方案
From: https://blog.51cto.com/coderge/7438667

相关文章

  • SpringMVC 工作原理了解吗?
    request发送给dispatcherservletdispa~根据handlermapping找到handlerAdapterhandlerAdapter调用handler处理,返回modelandview给dispa~dispa将modelandview发送给视图解析器解析为逻辑view并返回给dispadispa~根据逻辑的view对象找到真正的view对象并且用真正的v......
  • 简单介绍 Spring MVC 的核心组件
    DispatcherServlet和九大组件(按使用顺序排序的):组件说明DispatcherServletSpringMVC的核心组件,是请求的入口,负责协调各个组件工作MultipartResolver内容类型(Content-Type)为multipart/*的请求的解析器,例如解析处理文件上传的请求,便于获取参数信息以及上传的......
  • Spring 事务
    Spring事务实现方式有哪些编程式优点:灵活缺点:麻烦,难以维护声明式加注解Spring的事务管理有什么优点支持声明式事务管理提供跨不同事务api的一致事务模型传播规则借用别人的的图片方便记忆支持当前事务的“女生”,这里的事务指的是“房子”,它分为3种(普通型......
  • SSM SpringBoot vue快递柜管理系统
    SSMSpringBootvue快递柜管理系统系统功能登录注册个人中心快递员管理用户信息管理 用户寄件管理配送信息管理寄存信息管理开发环境和技术开发语言:Java使用框架:SSM(Spring+SpringMVC+Mybaits)或SpringBoot前端:vue数据库:Mysql架构:B/S源码类型......
  • Redis 缓存击穿,缓存穿透,缓存雪崩原因+解决方案
    缓存击穿,缓存穿透,缓存雪崩的原因缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存......
  • Spring中@Autowired、@Qualifier、@Resource、@Primary、@Inject注解的区别
       @Primary注解在Spring框架中用于解决自动装配冲突。当存在多个类型相同的Bean时,如果没有使用@Qualifier注解或者@Qualifier注解没有指定Bean的名称,Spring框架将无法判断应该使用哪个Bean进行注入。此时,可以使用@Primary注解来指定默认的Bean。被@Primary注解的Bean会优......
  • spring和springboot注解的使用
    spring和springboot注解的使用首先看一下ElementType取值范围://用于描述类、接口(包括注解类型)或enum声明TYPE,//用于描述域FIELD,//用于描述方法METHOD,//用于描述参数PARAMETER, //用于描述构造器CONSTRUCTOR,//用于描述局部变量......
  • SpringBoot-Learning系列之Kafka整合
    SpringBoot-Learning系列之Kafka整合本系列是一个独立的SpringBoot学习系列,本着WhatWhyHow的思想去整合Java开发领域各种组件。消息系统主要应用场景流量消峰(秒杀抢购)、应用解耦(核心业务与非核心业务之间的解耦)异步处理、顺序处理实时数据传输管道异构语言架构......
  • SpringBoot集成微信支付JSAPIV3保姆教程
    前言最近为一个公众号h5商城接入了微信支付功能,查找资料过程中踩了很多坑,以此文章记录一下和大家分享前期准备公众号认证微信支付功能需要开通企业号并进行资质认证,费用一年300,且需企业营业执照等信息,对公账户打款验证登录微信公众平台https://mp.weixin.qq.com/,创建服务号如......
  • SpringBoot集成微信支付JSAPIV3保姆教程
    前言最近为一个公众号h5商城接入了微信支付功能,查找资料过程中踩了很多坑,以此文章记录一下和大家分享前期准备公众号认证微信支付功能需要开通企业号并进行资质认证,费用一年300,且需企业营业执照等信息,对公账户打款验证登录微信公众平台https://mp.weixin.qq.com/,创建服务号......