展开说说,Spring Bean IOC、AOP 循环依赖

做者:小傅哥
博客:https://bugstack.cnhtml

沉淀、分享、成长,让本身和他人都能有所收获!😄

1、前言

延迟知足能给你带来什么?java

大学有四年时间,但几乎全部人都是临近毕业才发现找一份好工做费劲,尤为是我能很是熟悉的软件开发行业,即便是毕业了还须要额外花钱到培训机构,在学一遍编程技术才能出去找工做。好像在校这几年压根就没学到什么!git

就我我的而言多是由于上学期间喜欢编程,也从师哥、师姐那里听到一些关于毕业后找工做的不容易,也了解了一些社会上对程序员开发技能的要求级别。也就是获得了这些消息,又加上本身乐于折腾,我给本身定了一个天天都能完成的小目标:程序员

红尘世界几个王,我自不服迎头上。
日敲代码两百行,冲进世界五百强。

哈哈哈,就这么天天两百行代码,一个月就是6千行,一年就是6万行,三年后开始实习就有18万行,一个应届实习生有将近20万行代码的敲击量,几乎已经能够很是熟练的完成各种简单的工做,在加上实习中对整个项目流程真正的断链后,找一个正经的开发工做,仍是很容易的。github

而这时候找工做的容易,就来自于你一直以来的学习和沉淀,但若是你没通过这些努力,可能等毕业后就会变得很是慌乱,最后没办法只能去一些机构再学习一遍。面试

2、面试题

谢飞机,小记!,之前感受Spring没啥,看过一篇getBean,个人天!spring

谢飞机:面试官,最近我看了 Spring 的 getBean 发现这里好多东西,还有一个是要解决循环依赖的,这玩意面试有啥要问的吗?编程

面试官:有哇,Spring 是如何解决循环依赖的?设计模式

谢飞机:嗯,经过三级缓存提早暴露对象解决的。缓存

面试官:能够哈,那这三个缓存里都存放了什么样的对象信息呢?

谢飞机:一级缓存存放的是完整对象,也叫成品对象。二级缓存存放的是半成品对象,就是那些属性还没赋值的对象。三级缓存存放的是 ObjectFactory<?> 类型的 lambda 表达式,就是这用于处理 AOP 循环依赖的。

面试官:能够呀,谢飞机有所准备嘛!那若是没有三级缓存,只有二级或者一级,能解决循环依赖吗?

谢飞机:其实我看过资料了,能够解决,只不过 Spring 要保证几个事情,只有一级缓存处理流程无法拆分,复杂度也会增长,同时半成品对象可能会有空指针异常。而将半成品与成品对象分开,处理起来也更加优雅、简单、易扩展。另外 Spring 的两大特性中不只有 IOC 还有 AOP,也就是基于字节码加强后的方法,该存放到哪,而三级缓存最主要,要解决的循环依赖就是对 AOP 的处理,但若是把 AOP 代理对象的建立提早,那么二级缓存也同样能够解决。可是,这就违背了 Spring 建立对象的原则,Spring 更喜欢把全部的普通 Bean 都初始化完成,在处理代理对象的初始化。

面试官:飞机,不错嘛,此次了解了很多。那问个简单的,你撸过循环依赖的解决方案?

谢飞机:哦哦,这没有,没实践过!!!确实应该搞一下,试试。

3、什么是循环依赖?

1. 问题描述

了解问题的本质再分析问题,每每更利于对问题有更深刻的了解和研究。因此咱们在分析 Spring 关于循环依赖的源码以前,先要了解下什么是循环依赖。

  • 循环依赖分为三种,自身依赖于自身、互相循环依赖、多组循环依赖。
  • 但不管循环依赖的数量有多少,循环依赖的本质是同样的。就是你的完整建立依赖于我,而个人完整建立也依赖于你,但咱们互相无法解耦,最终致使依赖建立失败。
  • 因此 Spring 提供了除了构造函数注入和原型注入外的,setter循环依赖注入解决方案。那么咱们也能够先来尝试下这样的依赖,若是是咱们本身处理的话该怎么解决。

2. 问题体现

public class ABTest {

    public static void main(String[] args) {
        new ClazzA();
    }

}

class ClazzA {

    private ClazzB b = new ClazzB();

}

class ClazzB {

    private ClazzA a = new ClazzA();

}
  • 这段代码就是循环依赖最初的模样,你中有我,我中有你,运行就报错 java.lang.StackOverflowError
  • 这样的循环依赖代码是无法解决的,当你看到 Spring 中提供了 get/set 或者注解,这样之因此能解决,首先是进行了必定的解耦。让类的建立和属性的填充分离,先建立出半成品Bean,再处理属性的填充,完成成品Bean的提供。

3. 问题处理

在这部分的代码中就一个核心目的,咱们来本身解决一下循环依赖,方案以下:

public class CircleTest {

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

    public static void main(String[] args) throws Exception {
        System.out.println(getBean(B.class).getA());
        System.out.println(getBean(A.class).getB());
    }

    private static <T> T getBean(Class<T> beanClass) throws Exception {
        String beanName = beanClass.getSimpleName().toLowerCase();
        if (singletonObjects.containsKey(beanName)) {
            return (T) singletonObjects.get(beanName);
        }
        // 实例化对象入缓存
        Object obj = beanClass.newInstance();
        singletonObjects.put(beanName, obj);
        // 属性填充补全对象
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Class<?> fieldClass = field.getType();
            String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
            field.set(obj, singletonObjects.containsKey(fieldBeanName) ? singletonObjects.get(fieldBeanName) : getBean(fieldClass));
            field.setAccessible(false);
        }
        return (T) obj;
    }

}

class A {

    private B b;

    // ...get/set
}

class B {
    private A a;

        // ...get/set
}
  • 这段代码提供了 A、B 两个类,互相有依赖。但在两个类中的依赖关系使用的是 setter 的方式进行填充。也就是只有这样才能避免两个类在建立之初不非得强依赖于另一个对象。
  • getBean,是整个解决循环依赖的核心内容,A 建立后填充属性时依赖 B,那么就去建立 B,在建立 B 开始填充时发现依赖于 A,但此时 A 这个半成品对象已经存放在缓存到singletonObjects 中了,因此 B 能够正常建立,在经过递归把 A 也建立完整了。

4、源码分析

1. 说说细节

经过上面的例子咱们大概了解到,A和B互相依赖时,A建立完后填充属性B,继续建立B,再填充属性A时就能够从缓存中获取了,以下:

那这个解决事循环依赖的事放到 Spring 中是什么样呢?展开细节!

虽然,解决循环依赖的核心原理同样,但要放到支撑起整个 Spring 中 IOC、AOP 特性时,就会变得复杂一些,整个处理 Spring 循环依赖的过程以下;

  • 以上就是关于 Spring 中对于一个有循环依赖的对象获取过程,也就是你想要的说说细节
  • 乍一看是挺多流程,可是这些也基本是你在调试代码时候必须通过的代码片断,拿到这份执行流程,再调试就很是方便了。

2. 处理过程

关于本章节涉及到的案例源码分析,已更新到 github:https://github.com/fuzhengwei/interview - interview-31

如下是单元测试中对AB依赖的获取Bean操做,重点在于进入 getBean 的源码跟进;

@Test
public void test_alias() {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    Bean_A bean_a = beanFactory.getBean("bean_a", Bean_A.class);
    logger.info("获取 Bean 经过别名:{}", bean_a.getBean_b());
}

org.springframework.beans.factory.support.AbstractBeanFactory.java

@Override
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    return doGetBean(name, requiredType, null, false);
}
  • 从 getBean 进入后,获取 bean 的操做会进入到 doGetBean。
  • 之因此这样包装一层,是由于 doGetBean 有不少不一样入参的重载方法,方便外部操做。

doGetBean 方法

protected <T> T doGetBean(
        final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
        throws BeansException {
    
  // 从缓存中获取 bean 实例
    Object sharedInstance = getSingleton(beanName);
    
            // mbd.isSingleton() 用于判断 bean 是不是单例模式
            if (mbd.isSingleton()) {
              // 获取 bean 实例
                sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
                    @Override
                    public Object getObject() throws BeansException {
                        try {
                          // 建立 bean 实例,createBean 返回的 bean 实例化好的
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            destroySingleton(beanName);
                            throw ex;
                        }
                    }
                });
                // 后续的处理操做
                bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            }
            
    // ...

  // 返回 bean 实例
    return (T) bean;
}
  • 按照在源码分析的流程图中能够看到,这一部分是从 getSingleton 先判断是否有实例对象,对于第一次进入是确定没有对象的,要继续往下走。
  • 在判断 mbd.isSingleton() 单例之后,开始使用基于 ObjectFactory 包装的方式建立 createBean,进入后核心逻辑是开始执行 doCreateBean 操做。

doCreateBean 方法

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
        throws BeanCreationException {
    
      // 建立 bean 实例,并将 bean 实例包装到 BeanWrapper 对象中返回
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    
        // 添加 bean 工厂对象到 singletonFactories 缓存中
        addSingletonFactory(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
              // 获取原始对象的早期引用,在 getEarlyBeanReference 方法中,会执行 AOP 相关逻辑。若 bean 未被 AOP 拦截,getEarlyBeanReference 原样返回 bean。
                return getEarlyBeanReference(beanName, mbd, bean);
            }
        });
        
    try {
      // 填充属性,解析依赖关系
        populateBean(beanName, mbd, instanceWrapper);
        if (exposedObject != null) {
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        }
    }
    
    // 返回 bean 实例
    return exposedObject;
}
  • 在 doCreateBean 方法中包括的内容较多,但核心主要是建立实例、加入缓存以及最终进行属性填充,属性填充就是把一个 bean 的各个属性字段涉及到的类填充进去。
  • createBeanInstance,建立 bean 实例,并将 bean 实例包装到 BeanWrapper 对象中返回
  • addSingletonFactory,添加 bean 工厂对象到 singletonFactories 缓存中
  • getEarlyBeanReference,获取原始对象的早期引用,在 getEarlyBeanReference 方法中,会执行 AOP 相关逻辑。若 bean 未被 AOP 拦截,getEarlyBeanReference 原样返回 bean。
  • populateBean,填充属性,解析依赖关系。也就是从这开始去找寻 A 实例中属性 B,紧接着去建立 B 实例,最后在返回回来。

getSingleton 三级缓存

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // 从 singletonObjects 获取实例,singletonObjects 是成品 bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // 判断 beanName ,isSingletonCurrentlyInCreation 对应的 bean 是否正在建立中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
          // 从 earlySingletonObjects 中获取提早曝光未成品的 bean
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
              // 获取相应的 bean 工厂
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                  // 提早曝光 bean 实例,主要用于解决AOP循环依赖
                    singletonObject = singletonFactory.getObject();
                    
                    // 将 singletonObject 放入缓存中,并将 singletonFactory 从缓存中移除
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
  • singletonObjects.get(beanName),从 singletonObjects 获取实例,singletonObjects 是成品 bean
  • isSingletonCurrentlyInCreation,判断 beanName ,isSingletonCurrentlyInCreation 对应的 bean 是否正在建立中
  • allowEarlyReference,从 earlySingletonObjects 中获取提早曝光未成品的 bean
  • singletonFactory.getObject(),提早曝光 bean 实例,主要用于解决AOP循环依赖

综上,是一个处理循环依赖的代码流程,这部分提取出来的内容主要为核心内容,并没与长篇大论的所有拆取出来,你们在调试的时候会涉及的比较多,尽量要本身根据流程图操做调试几遍。

3. 依赖解析

综上从咱们本身去尝试解决循环依赖,学习了循环依赖的核心解决原理。又分析了 Spring 解决的循环依赖的处理过程以及核心源码的分析。那么接下来咱们在总结下三级缓存分别不一样的处理过程,算是一个总结,也方便你们理解。

1. 一级缓存能解决吗?

  • 其实只有一级缓存并非不能解决循环依赖,就像咱们本身作的例子同样。
  • 可是在 Spring 中若是像咱们例子里那么处理,就会变得很是麻烦,并且也可能会出现 NPE 问题。
  • 因此如图按照 Spring 中代码处理的流程,咱们去分析一级缓存这样存放成品 Bean 的流程中,是不能解决循环依赖的问题的。由于 A 的成品建立依赖于 B,B的成品建立又依赖于 A,当须要补全B的属性时 A 仍是没有建立完,因此会出现死循环。

2. 二级缓存能解决吗?

  • 有了二级缓存其实这个事处理起来就容易了,一个缓存用于存放成品对象,另一个缓存用于存放半成品对象。
  • A 在建立半成品对象后存放到缓存中,接下来补充 A 对象中依赖 B 的属性。
  • B 继续建立,建立的半成品一样放到缓存中,在补充对象的 A 属性时,能够从半成品缓存中获取,如今 B 就是一个完整对象了,而接下来像是递归操做同样 A 也是一个完整对象了。

3. 三级缓存解决什么?

  • 有了二级缓存都能解决 Spring 依赖了,怎么要有三级缓存呢。其实咱们在前面分析源码时也提到过,三级缓存主要是解决 Spring AOP 的特性。AOP 自己就是对方法的加强,是 ObjectFactory<?> 类型的 lambda 表达式,而 Spring 的原则又不但愿将此类类型的 Bean 前置建立,因此要存放到三级缓存中处理。
  • 其实总体处理过程相似,惟独是 B 在填充属性 A 时,先查询成品缓存、再查半成品缓存,最后在看看有没有单例工程类在三级缓存中。最终获取到之后调用 getObject 方法返回代理引用或者原始引用。
  • 至此也就解决了 Spring AOP 所带来的三级缓存问题。本章节涉及到的 AOP 依赖有源码例子,能够进行调试

5、总结

  • 回顾本文基本以实际操做的例子开始,引导你们对循环依赖有一个总体的认识,也对它的解决方案能够上手的例子,这样对后续的关于 Spring 对循环依赖的解决也就不会那么陌生了。
  • 通篇全文下来你们也能够看到,三级缓存并非非必须不可,只不过在知足 Spring 自身建立的原则下,是必须的。若是你能够下载 Spring 源码对这部分代码进行改动下,提早建立 AOP 对象保存到缓存中,那么二级缓存同样能够解决循环依赖问题。
  • 关于循环依赖可能并非一个好的编码方式,若是在本身的程序中仍是要尽量使用更合理的设计模式规避循环依赖,可能这些方式会增长代码量,但在维护上会更加方便。固然这不是强制,能够根据你的须要而来。

6、系列推荐

相关文章
相关标签/搜索