手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

本文通过实例分析了Spring中如何解决单例模式下的循环依赖问题,介绍了使用两级缓存解决多线程环境下的安全性问题,并探讨了Spring的三级缓存机制及其作用,揭示了解决循环依赖的本质——借助于缓存。同时也讨论了构造器注入与set注入在循环依赖处理上的差异。

先不去管Spring中的循环依赖,我们先实现一个自定义注解,来模拟@Autowired的功能。


一、自定义注解模拟@Autowired

自定义Load注解,被该注解标识的字段,将会进行自动注入

/**
 * @author qcy
 * @create 2021/10/02 13:31:20
 */
//只用在字段上
@Target(ElementType.FIELD)
//运行时有效,这样可以通过反射解析注解
@Retention(RetentionPolicy.RUNTIME)
public @interface Load {
}

新建A类与B类,其中A类中需要注入B

public class A {
    @Load
    private B b;
 
    public B getB() {
        return b;
    }
}
 
 
public class B {
}

测试类

public class Main {
 
    private static <T> T getBean(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        //实例化对象
        T instance = clazz.newInstance();
 
        //获取当前类中的所有字段
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            //允许访问私有变量
            field.setAccessible(true);
            //判断字段是否被@Load注解修饰
            boolean isUseLoad = field.isAnnotationPresent(Load.class);
            if (!isUseLoad) {
                continue;
            }
            //获取需要被注入的字段的class
            Class<?> fieldType = field.getType();
            //递归获取字段的实例对象
            Object fieldBean = getBean(fieldType);
            //将实例对象注入到该字段中
            field.set(instance, fieldBean);
        }
 
        return instance;
    }
 
 
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        A a = getBean(A.class);
        System.out.println(a.getB().getClass());
    }
 

最终能够打印出a对象中依赖的b的class类型

二、多例模式下的循环依赖

现在思考一个问题,如果b对象同时依赖a呢?也就是B类中需要注入A

现在直接把B类的代码改成以下的样子

public class B {
    @Load
    private A a;
 
    public A getA() {
        return a;
    }
}

直接运行测试类,会发生什么呢?

出现了栈溢出!到底是哪里出问题了呢?

原来是,在实例化A后,属性注入阶段发现需要注入B的实例,于是去实例化B,B又需要依赖A,因此去实例化A,一直依赖下去...

不难观察出,getBean每调用一次,都会返回一个新的对象,也就是对应于多例模式。

多例模式中出现循环依赖,直接报出了StackOverflowError,看来解决不了循环依赖,这也并不难理解

三、单例模式下使用缓存来解决循环依赖

如果这个时候,对于传入的同一个class,能够返回同一个实例,即单例模式,能否解决循环依赖呢?

大致的思路是,使用一个缓存map,将实例化好且属性注入完毕的对象缓存到该map中,下次直接使用即可。

可现在又遇到难题了,压根就创建不出来一个完整的A的实例对象啊,无法进行缓存。

既然无法直接将完成品放入到缓存中,那是否可以将实例对象分为两个阶段

半成品阶段

仅完成实例化,并没有完成属性注入

成品阶段

半成品完成属性注入

首先实例化A得到半成品a,接着将这个a放入到缓存中。然后实例化b时,注入缓存中半成品的a,得到成品b。最终再将成品b注入到半成品a中,此时a变为成品。

这个时候,a完成了实例化与属性注入,b也完成了实例化与属性注入,循环依赖好像就能解决了。

改下代码,直接上线!

public class Main {
 
    //由类名可以获取到对应的实例对象
    private static Map<String, Object> singletonObjects= new HashMap<>();
 
    private static <T> T getBean(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        //先从缓存中获取
        String className = clazz.getSimpleName();
        if (singletonObjects.containsKey(className)) {
            return (T) singletonObjects.get(className);
        }
 
        //实例化对象
        T instance = clazz.newInstance();
        //实例化完成后,就将这个半成品放入到缓存中
        singletonObjects.put(className
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值