专栏首页Java识堂Spring处理循环依赖只使用二级缓存可以吗?

Spring处理循环依赖只使用二级缓存可以吗?

什么是循环依赖?

先说一下什么是循环依赖,Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成

Spring的循环依赖有4种场景

  1. 构造器的循环依赖(singleton,prototype)
  2. 属性的循环依赖(singleton,prototype)

「spring目前只支持singleton类型的属性循环依赖」

构造器的循环依赖

@Component
public class ConstructorA {

 private ConstructorB constructorB;

 @Autowired
 public ConstructorA(ConstructorB constructorB) {
  this.constructorB = constructorB;
 }
}
@Component
public class ConstructorB {

 private ConstructorA constructorA;

 @Autowired
 public ConstructorB(ConstructorA constructorA) {
  this.constructorA = constructorA;
 }
}
@Configuration
@ComponentScan("com.javashitang.dependency.constructor")
public class ConstructorConfig {
}
public class ConstructorMain {

 public static void main(String[] args) {
  AnnotationConfigApplicationContext context =
    new AnnotationConfigApplicationContext(ConstructorConfig.class);
  System.out.println(context.getBean(ConstructorA.class));
  System.out.println(context.getBean(ConstructorB.class));
 }
}

运行ConstructorMain的main方法的时候会在第一行就报异常,说明Spring没办法初始化所有的Bean,即上面这种形式的循环依赖Spring无法解决。

「构造器的循环依赖,可以在构造函数中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入」

@Autowired
public ConstructorB(@Lazy ConstructorA constructorA) {
 this.constructorA = constructorA;
}

因为我们主要关注属性的循环依赖,构造器的循环依赖就不做过多分析了

属性的循环依赖

先演示一下什么是属性的循环依赖

@Data
@Component
public class A {

    @Autowired
    private B b;
}
@Data
@Component
public class B {

    @Autowired
    private A a;
}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.javashitang.dependency")
public class Config {
}
public class Main {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(Config.class);
        System.out.println(context.getBean(A.class).getB() == context.getBean(B.class));
        System.out.println(context.getBean(B.class).getA() == context.getBean(A.class));
    }
}

Spring容器正常启动,运行结果为true,想实现类似的功能并不难,我写个demo演示一下

public class DependencyDemoV1 {

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

    @SneakyThrows
    public static <T> T getBean(Class<T> beanClass) {
        String beanName = beanClass.getSimpleName();
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        // 实例化bean
        Object object = beanClass.getDeclaredConstructor().newInstance();
        singletonObjects.put(beanName, object);
        // 开始初始化bean,即填充属性
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            // 获取需要注入字段的class
            Class<?> fieldClass = field.getType();
            field.set(object, getBean(fieldClass));
        }
        return (T) object;
    }

    public static void main(String[] args) {
        // 假装扫描出来的类
        Class[] classes = {A.class, B.class};
        for (Class aClass : classes) {
            getBean(aClass);
        }
        System.out.println(getBean(A.class).getB() == getBean(B.class));
        System.out.println(getBean(B.class).getA() == getBean(A.class));
    }

}

「在开始后面的内容的时候,我们先明确2个概念」

实例化:调用构造函数将对象创建出来 初始化:调用构造函数将对象创建出来后,给对象的属性也被赋值

可以看到只用了一个map就实现了循环依赖的实现,但这种实现有个小缺陷,singletonObjects中的类有可能只是完成了实例化,并没有完成初始化

而在spring中singletonObjects中的类都完成了初始化,因为我们取单例Bean的时候都是从singletonObjects中取的,不可能让我们获取到没有初始化完成的对象。

所以我们来写第二个实现,「用singletonObjects存初始化完成的对象,而用earlySingletonObjects暂存实例化完成的对象,等对象初始化完毕再将对象放入singletonObjects,并从earlySingletonObjects删除」

public class DependencyDemoV2 {

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

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

    @SneakyThrows
    public static <T> T getBean(Class<T> beanClass) {
        String beanName = beanClass.getSimpleName();
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        if (earlySingletonObjects.containsKey(beanName)) {
            return (T) earlySingletonObjects.get(beanName);
        }
        // 实例化bean
        Object object = beanClass.getDeclaredConstructor().newInstance();
        earlySingletonObjects.put(beanName, object);
        // 开始初始化bean,即填充属性
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            // 获取需要注入字段的class
            Class<?> fieldClass = field.getType();
            field.set(object, getBean(fieldClass));
        }
        singletonObjects.put(beanName, object);
        earlySingletonObjects.remove(beanName);
        return (T) object;
    }

    public static void main(String[] args) {
        // 假装扫描出来的类
        Class[] classes = {A.class, B.class};
        for (Class aClass : classes) {
            getBean(aClass);
        }
        System.out.println(getBean(A.class).getB() == getBean(B.class));
        System.out.println(getBean(B.class).getA() == getBean(A.class));
    }

}

现在的实现和spring保持一致了,并且只用了2级缓存。spring为什么搞第三个缓存呢?「第三个缓存主要和代理对象相关」

我还是把上面的例子改进一下,改成用3级缓存的实现

public interface ObjectFactory<T> {
    T getObject();
}
public class DependencyDemoV3 {

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

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

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

    @SneakyThrows
    public static <T> T getBean(Class<T> beanClass) {
        String beanName = beanClass.getSimpleName();
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        if (earlySingletonObjects.containsKey(beanName)) {
            return (T) earlySingletonObjects.get(beanName);
        }
        ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
        if (singletonFactory != null) {
            return (T) singletonFactory.getObject();
        }
        // 实例化bean
        Object object = beanClass.getDeclaredConstructor().newInstance();
        singletonFactories.put(beanName, () -> {
            Object proxy = createProxy(object);
            singletonFactories.remove(beanName);
            earlySingletonObjects.put(beanName, proxy);
            return proxy;
        });
        // 开始初始化bean,即填充属性
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            // 获取需要注入字段的class
            Class<?> fieldClass = field.getType();
            field.set(object, getBean(fieldClass));
        }
        createProxy(object);
        singletonObjects.put(beanName, object);
        singletonFactories.remove(beanName);
        earlySingletonObjects.remove(beanName);
        return (T) object;
    }

    public static Object createProxy(Object object) {
        // 因为这个方法有可能被执行2次,所以这里应该有个判断
        // 如果之前提前进行过aop操作则直接返回,知道意思就行,不写了哈
        // 需要aop的话则返回代理对象,否则返回传入的对象
        return object;
    }

    public static void main(String[] args) {
        // 假装扫描出来的类
        Class[] classes = {A.class, B.class};
        for (Class aClass : classes) {
            getBean(aClass);
        }
        System.out.println(getBean(A.class).getB() == getBean(B.class));
        System.out.println(getBean(B.class).getA() == getBean(A.class));
    }

}

「为什么要包装一个ObjectFactory对象?」

如果创建的Bean有对应的aop代理,那其他对象注入时,注入的应该是对应的代理对象;「但是Spring无法提前知道这个对象是不是有循环依赖的情况」,而正常情况下(没有循环依赖情况),Spring都是在对象初始化后才创建对应的代理。这时候Spring有两个选择:

  1. 不管有没有循环依赖,实例化后就直接创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入(只需要2级缓存,singletonObjects和earlySingletonObjects即可)
  2. 「不提前创建好代理对象,在出现循环依赖被其他对象注入时,才提前生成代理对象(此时只完成了实例化)。这样在没有循环依赖的情况下,Bean还是在初始化完成才生成代理对象」(需要3级缓存)

「所以到现在为止你知道3级缓存的作用了把,主要是为了正常情况下,代理对象能在初始化完成后生成,而不用提前生成」

缓存

说明

singletonObjects

第一级缓存,存放初始化完成的Bean

earlySingletonObjects

第二级缓存,存放实例化完成的Bean,有可能被进行了代理

singletonFactories

延迟生成代理对象

源码解析

获取Bean的时候先尝试从3级缓存中获取,和我们上面的Demo差不多哈

DefaultSingletonBeanRegistry#getSingleton

当从缓存中获取不到时,会进行创建 AbstractAutowireCapableBeanFactory#doCreateBean(删除了部分代码哈)

发生循环依赖时,会从工厂里获取代理对象哈

当开启aop代理时,SmartInstantiationAwareBeanPostProcessor的一个实现类有AbstractAutoProxyCreator

AbstractAutoProxyCreator#getEarlyBeanReference

getEarlyBeanReference方法提前进行代理,为了防止后面再次进行代理,需要用earlyProxyReferences记录一下,这个Bean已经被代理过了,不用再代理了

AbstractAutoProxyCreator#postProcessAfterInitialization

这个方法是进行aop代理的地方,因为有可能提前代理了,所以先根据earlyProxyReferences判断一下,是否提前代理了,提前代理过就不用代理了

当bean初始化完毕,会放入一级缓存,并从二三级缓存删除

DefaultSingletonBeanRegistry#addSingleton

发生循环依赖时,整体的执行流程如下

您的“点赞/在看/分享”是我坚持的最大动力!

坚持不易,卖萌打滚求鼓励 (ฅ>ω<*ฅ)

文章分享自微信公众号:
Java识堂

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:李立敏
原始发表时间:2022-01-12
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 【死磕 Spring】—– IOC 之循环依赖处理

    这篇分析 doCreateBean() 第三个过程:循环依赖处理。其实循环依赖并不仅仅只是在 doCreateBean() 中处理,其实在整个加载 bean 的...

    用户1655470
  • Spring循环依赖三级缓存是否可以减少为二级缓存?

    我们都知道Spring通过三级缓存来解决循环依赖的问题,那么是不是必须是三级缓存?二级缓存不能解决吗?

    程序员白楠楠
  • spring(5) - Bean实例化中,对循环依赖的处理

    (1) spring(3) - springboot启动过程 (2) spring(4) - spring Bean创建并注册到容器

    惊羽-布壳儿
  • Spring 为何需要三级缓存解决循环依赖,而不是二级缓存?

    我们在使用Spring框架的日常开发中,bean之间的循环依赖太频繁了,Spring已经帮我们去解决循环依赖问题,对我们开发者来说是无感知的,下面具体分析一下S...

    田维常
  • Spring Ioc源码分析 之 Bean的加载(六):循环依赖处理

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

    大王叫下
  • 京东一面:Spring 为何需要三级缓存解决循环依赖,而不是二级缓存?我懵了。。

    点击上方“芋道源码”,选择“设为星标” 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 10:33 更新文章,每天掉亿点点头发... 源码精品专栏 原创 |...

    芋道源码
  • Spring如何通过三级缓存解决循环依赖

    这个例子存在的问题:理论上spring创建A的时候依赖了B,然后spring就会去加载B,但是这个时候B又依赖了A,spring又去加载A,就会陷入一个死循环,...

    诺浅
  • 烂了大街的 Spring 循环依赖问题,你觉得自己会了吗

    初学 Spring 的时候,我们就知道 IOC,控制反转么,它将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,不需要我们手动去各种 new ...

    海星
  • Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗

      做 Java 开发的,一般都绕不开 Spring,那么面试中肯定会被问到 Spring 的相关内容,而循环依赖又是 Spring 中的高频面试题

    青石路
  • 3.4 spring5源码系列--循环依赖的设计思想

    第一篇: 3.1 spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖

    用户7798898
  • 《我想进大厂》之Spring夺命连环10问

    工厂模式:工厂模式主要是通过 BeanFactory 和 ApplicationContext 来生产 Bean 对象。

    艾小仙
  • 烂大街的Spring循环依赖该如何回答?

    从字面上来理解就是A依赖B的同时B也依赖了A,就像上面这样,或者C依赖与自己本身。体现到代码层次就是这个样子

    sowhat1412
  • 循环依赖 之 手写代码模拟spring循环依赖

    如上图: A依赖了B, B同时依赖了A和C , C依赖了A. 这也是循环依赖. , 形成了一个闭环

    用户5546570
  • Spring 中循环依赖是如何解决?

    支持不支持循环引用this.allowCircularReferences这个参数,可以在之前将参数修改为false;

    Tim在路上
  • 为了忽悠大厂面试官,熬夜总结了这些Spring面试题!

    如果说 Java 工程师,有什么一定要“死磕”拿下的东西,那一定是 Spring 无疑了。众所周知,Spring 无论在 Java 生态系统,还是在就业市场, ...

    程序员白楠楠
  • 3.1 spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖

    如上图: A依赖了B, B同时依赖了A和C , C依赖了A. 这也是循环依赖. , 形成了一个闭环

    用户7798898
  • 从源码角度,带你研究什么是三级缓存

    一级缓存里存的是成品对象,实例化和初始化都完成了,我们的应用中使用的对象就是一级缓存中的

    云扬四海
  • Spring循环依赖还能这么理解……

    多个bean之间相互依赖,形成了一个闭环。比如:A依赖于B、B依赖于C、C依赖于A。

    一条coding
  • Spring篇之循环依赖

    循环依赖就是多个Bean之间存在相互依赖,形成一个闭环,如下,PostService和UserService之间就存在相互依赖,这个依赖并不是方法 之间的依赖,...

    小四的技术之旅

扫码关注腾讯云开发者

领取腾讯云代金券