2

Spring解决循环依赖的思路

 1 year ago
source link: https://blog.51cto.com/u_16205813/7086948
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Spring解决循环依赖的思路

精选 原创

青丝高挽 2023-08-15 12:04:27 ©著作权

文章标签 属性注入 缓存 实例化 文章分类 Java 后端开发 阅读数258

近期在准备找一些新的工作机会,在网上看了一些面试常见问题,看看自己是否能比较好的回答。今天的这个问题:Spring如何解决循环依赖。

Spring解决循环依赖的思路_属性注入

看到网上的各种文章的发布时间,这个题目应该是老面试题了,可能比我的码龄长。有很多结合源码来进行解读的文章,但是大多数,是在描述Spring如何解决循环依赖,但是比较少会讲解为什么这么设计。今天自己写了一下简易的代码,结合这个简易的代码来说说我的理解。

什么是循环依赖

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

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

Spring解决循环依赖的思路_属性注入_02

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

Spring解决循环依赖的思路_属性注入_03

其实要打破上述这个循环的链条,关键点在于,将bean实例化和bean属性注入这2步分开,且允许在属性注入的时候,注入一个已经实例化但还未进行属性注入的bean。即让一个已经实例化的bean,提前暴露出来,可以被其他bean拿到引用进行属性注入,而这个提前暴露的bean的属性输入可以在后续过程中再完成,因为我们的目标bean在进行属性注入的时候,只要拿到这个提前暴露bean的引用即可。
这个思路也跟上面说的不支持构造方法输入的bean循环依赖是呼应的,因为实例化这一步就使用到了构造方法,如果是构造方法注入,这个bean都无法实例化出来,就没有可能进行提前暴露了。
顺着这个思路,我们很自然可以想到使用一个map来保存那些实例化之后的bean,这个bean可能仅仅是实例化,还未进行属性注入,其他bean如果依赖它,就可以从这map中获取到并进行注入。

Spring解决循环依赖的思路_缓存_04

根据上面的思路,我们尝试使用代码进行实现。核心是为了说明如何解决循环依赖,我们对其他部分做了一定的简化:定义2个类,BeanA和BeanB,BeanA中有个BeanB类型的属性b,BeanB中有个BeanA类型的属性a;我们的目标是使用上面解决循环依赖的思路,构造出2个的对象,且对象互相持有对方的引用。
// IMAGE 纸上得来终觉浅,觉知此事要躬行
我们先定义2个bean类,各自持有对方类型的一个属性:

@Data
public class BeanA {

    private BeanB b;
}
@Data
public class BeanB {

    private BeanA a;
}

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

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,我们直接从上往下解读,为了方便,我进行了截图和分块。

Spring解决循环依赖的思路_实例化_05

最上面,定义了一个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()方法,我们接下来看下这个方法

Spring解决循环依赖的思路_缓存_06
  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级

Spring中bean的创建过程比我们上述的代码示例会复杂很多,还有一个重要的步骤是,生成代理对象。我们上面构造出来的beanA和beanB都是原始类型BeanA和BeanB的对象,但是Spring中还有一个重要的功能点是aop,而aop就是通过动态代理,生成原始类型的代理类型,进而把原始对象包装成代理对象来实现的。我们上述的2级缓存,只处理的了原始对象,但是还未涉及到代理对象。
那这里又会有新的疑问,即使需要代理对象,我们可以进一步改造上述代码,在调用构造方法后,直接去生成代理对象,再放入二级缓存,这样,后面在属性注入步骤完成后,在一级缓存中放入的也是代理对象,这样不也是可以使用二级缓存来解决循环依赖吗?
这个问题我也思考了很久,看了网上很多资料,目前我思考的结论是:这是为了满足Spring的1种设计的思想:尽量延迟去生成代理对象。如果在2级缓存中,提前暴露的就是代理对象,只从解决循环依赖这个问题的角度,应该是可行的,但是,这样让这个代理对象提前暴露,可能会带来额外的一些安全风险,不满足尽量延迟去生成代理对象这一指导思想。这一点,如果大家有别的见解,我们一起在评论区讨论。

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

看到这里了,点个赞再走呗

Spring解决循环依赖的思路_缓存_07

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK