风也温柔

计算机科学知识库

java反射动态注入方法-手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘

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

  一、自定义注解模拟@

  自定义Load注解,被该注解标识的字段java反射动态注入方法,将会进行自动注入

   /**

     * @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 getBean(Class clazz) throws IllegalAccessException, InstantiationException {
            //[实例化][13]对象
            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又需要依赖Ajava反射动态注入方法,因此去实例化A,一直依赖下去...

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

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

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

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

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

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

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

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

  半成品阶段

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

  成品阶段

  半成品完成属性注入

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

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

  改下代码,直接上线!

   public class Main {

     
        //由类名可以获取到对应的实例对象
        private static Map singletonObjects= new HashMap();
     
        private static  T getBean(Class clazz) throws IllegalAccessException, InstantiationException {
            //先从缓存中获取
            String className = clazz.getSimpleName();
            if (singletonObjects.containsKey(className)) {
                return (T) singletonObjects.get(className);
            }
     
            //实例化对象
            T instance = clazz.newInstance();
            //实例化完成后,就将这个半成品放入到缓存中
            singletonObjects.put(className, instance);
     
            //获取当前类中的所有字段
            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 a1 = getBean(A.class);
            A a2 = getBean(A.class);
     
            System.out.println(a1 == a2);
            System.out.println(a1.getB() == a1.getB());
     
            B b = getBean(B.class);
            System.out.println(a1.getB() == b);
        }
        

  运行后,将返回三个true,说明单例模式下的循环依赖是可以解决的。

  大致的图是这样的

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

  事情似乎到这里应该结束了,好的观众朋友们,咱们下期见。

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

  四、多线程下隐藏的问题

  以上的代码,在单线程的环境下,是没有问题的。可是放到多线程的环境中,可能就会出现空指针问题。

  线程1刚把半成品a放入到缓存中,还未来得及将b注入进去。此时线程2直接在缓存中获取到了a,在尝试调用其所依赖的b的任何方法时,就会出现空指针异常。

  因此上述代码,存在线程不安全的问题,怎么去解决呢?

  很简单,我直接对方法加锁不就可以了吗?

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

  对整个方法加锁确实可以解决问题,但运行性能会大打折扣。

  第一次的创建与读缓存互斥,创建完成后的读与读不需要加锁。

  其实问题的本质在于,缓存既存放半成品类型,又存放成品类型,导致线程根本不清楚拿到的实例的类型。

  那能不能再创建一个缓存s,即早期单例对象,就存放半成品类型,先前的存放成品类型。

  直接上代码

   //成品缓存

        private static final Map singletonObjects = new ConcurrentHashMap();
        //半成品缓存
        private static final Map earlySingletonObjects = new ConcurrentHashMap();
     
        //从缓存中获取
        private static Object getSingleton(String className) {
            //先从成品缓存中查找
            Object singletonObject = singletonObjects.get(className);
            if (singletonObject == null) {
                //再从半成品缓存中查找
                singletonObject = earlySingletonObjects.get(className);
            }
            return singletonObject;
        }
     
        @SuppressWarnings("unchecked")
        private static  T getBean(Class clazz) throws IllegalAccessException, InstantiationException {
            //先从缓存中获取
            String className = clazz.getSimpleName();
            Object singleton = getSingleton(className);
            if (singleton != null) {
                return (T) singleton;
            }
     
            synchronized (singletonObjects) {
                singleton = singletonObjects.get(className);
                //这里需要再进行一次检查
                if (singleton != null) {
                    return (T) singleton;
                }
     
                //实例化对象
                T instance = clazz.newInstance();
                //实例化完成后,就将这个半成品放入到缓存中
                earlySingletonObjects.put(className, instance);
     
                //获取当前类中的所有字段
                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);
                }
     
                //完成属性注入后,从半成品缓存中移除,加入到成品缓存中
                earlySingletonObjects.remove(className);
                singletonObjects.put(className, instance);
     
                return instance;
            }
        }
     
     
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    A a1 = getBean(A.class);
                    System.out.println("t1.a:" + a1.hashCode());
                    System.out.println("t1.b:" + a1.getB().hashCode());
                } catch (IllegalAccessException | InstantiationException e) {
                    e.printStackTrace();
                }
            }).start();
     
            new Thread(() -> {
                try {
                    A a1 = getBean(A.class);
                    System.out.println("t2.a:" + a1.hashCode());
                    System.out.println("t2.b:" + a1.getB().hashCode());
                } catch (IllegalAccessException | InstantiationException e) {
                    e.printStackTrace();
                }
            }).start();
     
            new Thread(() -> {
                try {
                    B b = getBean(B.class);
                    System.out.println("t3.b:" + b.hashCode());
                    System.out.println("t3.a:" + b.getA().hashCode());
                } catch (IllegalAccessException | InstantiationException e) {
                    e.printStackTrace();
                }
            }).start();

  输出结果:

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

  多次实验,从输出结果说明:在多线程的场景下,使用两级缓存能够有效避免出现空指针的问题,在一定程度上也能比整个方法加锁的效率更高。

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

  五、什么样的循环依赖都能解决吗?

  从第二节可以看出,多例模式下就不可以解决循环依赖。

  我们在以上小节中写的代码,是默认全部使用反射set注入的。

  而对于单例模式java反射动态注入方法-手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘,在经过对依赖项自然排序后,构造器注入是不可以优先于任何一个set注入的。

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

  第一种场景:b依赖a,需要使用构造器注入a;a依赖b,需要使用set注入b

  经过对Bean的自然排序后,会先去创建a,再去创建b。

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

  这种场景,是可以解决循环依赖的。在实例化B时,已经存在半成品a。

  结论:最后再使用构造器注入时,可以解决循环依赖。

  第二种场景:a依赖b,需要使用构造器注入b;b依赖a,需要使用set注入a

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

  这种场景,在实例化a时,就需要调用构造方法,因此去实例b,而b在缓存中找不到a,造成注入失败。

  结论:一开始就使用构造器注入,则不能解决循环依赖。

  那么都使用构造器注入时,那肯定也不能解决循环依赖的。

  因此,解决循环依赖有两个小前提:

  六、中解决循环依赖的原理

  在中,我们使用方法从容器获取一个Bean,那么就从方法入手

  类中的

   public Object getBean(String name) throws BeansException {

            assertBeanFactoryActive();
            return getBeanFactory().getBean(name);

  接着进入类中的

   public Object getBean(String name) throws BeansException {

            return doGetBean(name, null, null, false);

  再进入到方法中,该方法比较长,截取其中比较核心的点来说

  先说方法

   //从缓存中获取指定的bean

  进入到的方法中

   public Object getSingleton(String beanName) {

            return getSingleton(beanName, true);
        }
     
        protected Object getSingleton(String beanName, boolean allowEarlyReference) {
            //先从一级缓存中查找
            Object singletonObject = this.singletonObjects.get(beanName);
            //如果一级缓存中没有,且当前bean正处于创建的过程中
            if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
                synchronized (this.singletonObjects) {
                    //从二级缓存中查找
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    //如果二级缓存中也没有,且允许暴露早期引用时
                    if (singletonObject == null && allowEarlyReference) {
                        //从三级缓存中查找到bean的工厂
                        ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            //调用getObject方法生成bean
                            singletonObject = singletonFactory.getObject();
                            //放入到二级缓存中
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            //从三级缓存中移除
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
            return singletonObject;

  看到这里,似乎和之前我们写的代码很像啊。

  在解决循环依赖时,其实也用到了缓存,缓存声明及定义如下:

<p><pre> private final Map singletonObjects = new ConcurrentHashMap(256);

private final Map earlySingletonObjects = new HashMap(16);

private final Map