1 什么是循环依赖?

如下图所示:

BeanA类依赖了BeanB类,同时BeanB类又依赖了BeanA类。这种依赖关系形成了一个闭环,我们把这种依赖关系就称之为循环依赖。同理,再如下图的情况:

上图中,BeanA类依赖了BeanB类,BeanB类依赖了BeanC类,BeanC类依赖了BeanA类,如此,也形成了一个依赖闭环。再比如:

上图中,自己引用了自己,自己和自己形成了依赖关系。同样也是一个依赖闭环。那么,如果出现此类循环依赖的情况,会出现什么问题呢?

2 循环依赖问题复现

2.1 定义依赖关系

我们继续扩展前面的内容,给ModifyService增加一个属性,代码如下:

@GPService
public class ModifyService implements IModifyService {

  @GPAutowired private QueryService queryService;

  ...

}

给QueryService增加一个属性,代码如下:

@GPService
@Slf4j
public class QueryService implements IQueryService {

  @GPAutowired private ModifyService modifyService;

  ...

}

如此,ModifyService依赖了QueryService,同时QueryService也依赖了ModifyService,形成了依赖闭环。那么这种情况下会出现什么问题呢?

2.2 问题复现

我们来运行调试一下之前的代码,在GPApplicationContext初始化后打上断点,我们来跟踪一下IoC容器里面的情况,如下图:

启动项目,我们发现只要是有循环依赖关系的属性并没有自动赋值,而没有循环依赖关系的属性均有自动赋值,如下图所示:

这种情况是怎么造成的呢?我们分析原因之后发现,因为,IoC容器对Bean的初始化是根据BeanDefinition循环迭代,有一定的顺序。这样,在执行依赖注入时,需要自动赋值的属性对应的对象有可能还没初始化,没有初始化也就没有对应的实例可以注入。于是,就出现我们看到的情况。

3 使用缓存解决循环依赖问题

3.1 定义缓存

具体代码如下:

// 循环依赖的标识---当前正在创建的实例bean
  private Set<String> singletonsCurrectlyInCreation = new HashSet<String>();

  //一级缓存
  private Map<String, Object> singletonObjects = new HashMap<String, Object>();

  // 二级缓存: 为了将成熟的bean和纯净的bean分离. 避免读取到不完整的bean.
private Map<String, Object> earlySingletonObjects = new HashMap<String, Object>();

3.2 判断循环依赖

增加getSingleton()方法:

/**
    * 判断是否是循环引用的出口.
    * @param beanName
    * @return
    */
  private Object getSingleton(String beanName,GPBeanDefinition beanDefinition) {

      //先去一级缓存里拿,
      Object bean = singletonObjects.get(beanName);
      // 一级缓存中没有, 但是正在创建的bean标识中有, 说明是循环依赖
      if (bean == null && singletonsCurrentlyInCreation.contains(beanName)) {

          bean = earlySingletonObjects.get(beanName);
          // 如果二级缓存中没有, 就从三级缓存中拿
          if (bean == null) {
              // 从三级缓存中取
              Object object = instantiateBean(beanName,beanDefinition);

              // 然后将其放入到二级缓存中. 因为如果有多次依赖, 就去二级缓存中判断. 已经有了就不在再次创建了
              earlySingletonObjects.put(beanName, object);


          }
      }
      return bean;
  }

3.3 添加缓存

修改getBean()方法,在getBean()方法中添加如下代码:

         //Bean的实例化,DI是从而这个方法开始的
  public Object getBean(String beanName){

      //1、先拿到BeanDefinition配置信息
      GPBeanDefinition beanDefinition = regitry.beanDefinitionMap.get(beanName);

      // 增加一个出口. 判断实体类是否已经被加载过了
      Object singleton = getSingleton(beanName,beanDefinition);
      if (singleton != null) { return singleton; }

      // 标记bean正在创建
      if (!singletonsCurrentlyInCreation.contains(beanName)) {
          singletonsCurrentlyInCreation.add(beanName);
      }

      //2、反射实例化newInstance();
      Object instance = instantiateBean(beanName,beanDefinition);

      //放入一级缓存
      this.singletonObjects.put(beanName, instance);

      //3、封装成一个叫做BeanWrapper
      GPBeanWrapper beanWrapper = new GPBeanWrapper(instance);
      //4、执行依赖注入
      populateBean(beanName,beanDefinition,beanWrapper);
      //5、保存到IoC容器
      factoryBeanInstanceCache.put(beanName,beanWrapper);

      return beanWrapper.getWrapperInstance();

      }

3.4 添加依赖注入

修改populateBean()方法,代码如下:

    private void populateBean(String beanName, GPBeanDefinition beanDefinition, GPBeanWrapper beanWrapper) {

      ...

          try {

              //ioc.get(beanName) 相当于通过接口的全名拿到接口的实现的实例
              field.set(instance,getBean(autowiredBeanName));
          } catch (IllegalAccessException e) {
              e.printStackTrace();
              continue;
          }
      ...

  }

4 循环依赖对AOP创建代理对象的影响

4.1 循环依赖下的代理对象创建过程

我们都知道Spring AOP、事务等都是通过代理对象来实现的,而事务的代理对象是由自动代理创建器来自动完成的。也就是说Spring最终给我们放进容器里面的是一个代理对象,而非原始对象。

这里我们结合循环依赖,再分析一下AOP代理对象的创建过程和最终放进容器内的动作,看如下代码:

@Service
public class MyServiceImpl implements MyService {
  @Autowired
  private MyService myService;

  @Transactional
  @Override
  public Object hello(Integer id) {
      return "service hello";
  }
}

此Service类使用到了事务,所以最终会生成一个JDK动态代理对象Proxy。刚好它又存在自己引用自己的循环依赖的情况。跟进到Spring创建Bean的源码部分,来看doCreateBean()方法:

protected Object doCreateBean( ... ){

      ...

      // 如果允许循环依赖,此处会添加一个ObjectFactory到三级缓存里面,以备创建对象并且提前暴露引用
      // 此处Tips:getEarlyBeanReference是后置处理器SmartInstantiationAwareBeanPostProcessor的一个方法,
      // 主要是保证自己被循环依赖的时候,即使被别的Bean @Autowire进去的也是代理对象
      // AOP自动代理创建器此方法里会创建的代理对象

      // Eagerly cache singletons to be able to resolve circular references
      // even when triggered by lifecycle interfaces like BeanFactoryAware.
      boolean earlySingletonExposure = (mbd.isSingleton() &&
                                                  this.allowCircularReferences &&
                                                  isSingletonCurrentlyInCreation(beanName));
      if (earlySingletonExposure) { // 需要提前暴露(支持循环依赖),注册一个ObjectFactory到三级缓存
              addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
      }

      // 如果发现自己被循环依赖,会执行上面的getEarlyBeanReference()方法,从而创建一个代理对象从三级缓存转移到二级缓存里
      // 注意此时候对象还在二级缓存里,并没有在一级缓存。并且此时可以知道exposedObject仍旧是原始对象   populateBean(beanName, mbd, instanceWrapper);
      exposedObject = initializeBean(beanName, exposedObject, mbd);

      // 经过这两大步后,exposedObject还是原始对象
      // 注意:此处是以事务的AOP为例
      // 因为事务的AOP自动代理创建器在getEarlyBeanReference()创建代理后,
    // initializeBean() 就不会再重复创建了,二选一,下面会有详细描述)

      ...

      // 循环依赖校验(非常重要)
      if (earlySingletonExposure) {
              // 前面讲到因为自己被循环依赖了,所以此时候代理对象还存放在二级缓存中
              // 因此,此处getSingleton(),就会把代理对象拿出来
              // 然后赋值给exposedObject对象并返回,最终被addSingleton()添加进一级缓存中
              // 这样就保证了我们容器里缓存的对象实际上是代理对象,而非原始对象

              Object earlySingletonReference = getSingleton(beanName, false);
              if (earlySingletonReference != null) {

                      // 这个判断不可少(因为initializeBean()方法中给exposedObject对象重新赋过值,否则就是是两个不同的对象实例)
                      if (exposedObject == bean) {                
                              exposedObject = earlySingletonReference;
                      }
              }
              ...
      }

}

以上代码分析的是代理对象有自己存在循环依赖的情况,Spring用三级缓存很巧妙的进行解决了这个问题。

4.2 非循环依赖下的代理对象创建过程

如果自己并不存在循环依赖的情况,Spring的处理过程就稍微不同,继续跟进源码:

protected Object doCreateBean( ... ) {
... addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); ... // 此处注意,因为没有循环引用,所以上面getEarlyBeanReference()方法不会执行
// 也就是说此时二级缓存里并不会存在
populateBean(beanName, mbd, instanceWrapper); // 重点在此
//AnnotationAwareAspectJAutoProxyCreator自动代理创建器此处的postProcessAfterInitialization()方法里,会给创建一个代理对象返回
// 所以此部分执行完成后,exposedObject() 容器中缓存的已经是代理对象,不再是原始对象
// 此时二级缓存里依旧无它,更别提一级缓存了
exposedObject = initializeBean(beanName, exposedObject, mbd); ... // 循环依赖校验
if (earlySingletonExposure) {
// 前面讲到一级、二级缓存里都没有缓存,然后这里传参数是false,表示不从三级缓存中取值
// 因此,此时earlySingletonReference = null ,并直接返回 // 然后执行addSingleton()方法,由此可知,容器里最终存在的也还是代理对象 Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
}
...
}

根据以上代码分析可知,只要用到代理,没有被循环引用的,最终存在Spring容器里缓存的仍旧是代理对象。如果我们关闭Spring容器的循环依赖,也就是把allowCircularReferences设值为false,那么会不会出现问题呢?先关闭循环依赖开关。

// 它用于关闭循环引用(关闭后只要有循环引用现象将报错)
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { ((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(false); }
}

关闭循环依赖后,上面代码中存在A、B循环依赖的情况,运行程序会出现如下异常:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)

此处异常类型也是BeanCurrentlyInCreationException异常,但报错位置在DefaultSingletonBeanRegistry.beforeSingletonCreation

我们来分析一下,在实例化A后给其属性赋值时,Spring会去实例化B。B实例化完成后会继续给B属性赋值,由于我们关闭了循环依赖,所以不存在提前暴露引用。因此B无法直接拿到A的引用地址,只能又去创建A的实例。而此时我们知道A其实已经正在创建中了,不能再创建了。所有就出现了异常。对照演示代码,来分析一下程序运行过程:

@Service
public class MyServiceImpl implements MyService {

  // 因为关闭了循环依赖,所以此处不能再依赖自己
  // 但是MyService需要创建AOP代理对象
  //@Autowired
  //private MyService myService;

  @Transactional
  @Override
  public Object hello(Integer id) {
      return "service hello";
  }
}

其大致运行步骤如下:

protected Object doCreateBean( ... ) {

      // earlySingletonExposure = false 也就是Bean都不会提前暴露引用,因此不能被循环依赖

      boolean earlySingletonExposure = (mbd.isSingleton() &&
                                                  this.allowCircularReferences &&
                                                  isSingletonCurrentlyInCreation(beanName));
      ...

      populateBean(beanName, mbd, instanceWrapper);

      // 若是开启事务,此处会为原生Bean创建代理对象
      exposedObject = initializeBean(beanName, exposedObject, mbd);

      if (earlySingletonExposure) {
              ...

              // 因为上面没有提前暴露代理对象,所以上面的代理对象exposedObject直接返回。

      }
}

由上面代码可知,即使关闭循环依赖开关,最终缓存到容器中的对象仍旧是代理对象,显然@Autowired给属性赋值的也一定是代理对象。

最后,以AbstractAutoProxyCreator为例看看自动代理创建器实现循环依赖代理对象的细节。

AbstractAutoProxyCreator是抽象类,它的三大实现子类InfrastructureAdvisorAutoProxyCreator、AspectJAwareAdvisorAutoProxyCreator、AnnotationAwareAspectJAutoProxyCreator小伙伴们应该比较熟悉,该抽象类实现了创建代理的动作:

// 该类实现了SmartInstantiationAwareBeanPostProcessor接口 ,通过getEarlyBeanReference()方法解决循环引用问题

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

  ...

  // 下面两个方法是自动代理创建器创建代理对象的唯二的两个节点:

  // 提前暴露代理对象的引用,在postProcessAfterInitialization之前执行
  // 创建好后放进缓存earlyProxyReferences中,注意此处value是原始Bean

  @Override
  public Object getEarlyBeanReference(Object bean, String beanName) {

          Object cacheKey = getCacheKey(bean.getClass(), beanName);
          this.earlyProxyReferences.put(cacheKey, bean);
          return wrapIfNecessary(bean, beanName, cacheKey);

  }

  // 因为它会在getEarlyBeanReference之后执行,这个方法最重要的是下面的逻辑判断
  @Override
  public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {

      if (bean != null) {

          Object cacheKey = getCacheKey(bean.getClass(), beanName);

          // 下面的remove()方法返回被移除的value,也就是原始Bean
          // 判断如果存在循环引用,也就是执行了上面的getEarlyBeanReference()方法,
          // 此时remove() 返回值肯定是原始对象

          // 若没有被循环引用,getEarlyBeanReference()不执行
          // 所以remove() 方法返回null,此时进入if执行逻辑,调用创建代理对象方法
          if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                  return wrapIfNecessary(bean, beanName, cacheKey);
          }
      }

      return bean;

  }
  ...
}

根据以上分析可得知,自动代理创建器它保证了代理对象只会被创建一次,而且支持循环依赖的自动注入的依旧是代理对象。由上面分析得出结论,在Spring容器中,不论是否存在循环依赖的情况,甚至关闭Spring容器的循环依赖功能,它对Spring AOP代理的创建流程有影响,但对结果是无影响的。也就是说Spring很好地屏蔽了容器中对象的创建细节,让使用者完全无感知。

高频面试题:一张图彻底搞懂Spring循环依赖的更多相关文章

  1. 一张图彻底搞懂Spring循环依赖

    1 什么是循环依赖? 如下图所示: BeanA类依赖了BeanB类,同时BeanB类又依赖了BeanA类.这种依赖关系形成了一个闭环,我们把这种依赖关系就称之为循环依赖.同理,再如下图的情况: 上图中 ...

  2. 一张图彻底搞懂JavaScript的==运算

    一张图彻底搞懂JavaScript的==运算 来源 https://zhuanlan.zhihu.com/p/21650547 PS:最后,把图改了一下,仅供娱乐 : ) 大家知道,==是JavaSc ...

  3. 两张图彻底搞懂MyBatis的Mapper原理!

    作者:肥朝 简单使用 这是一个简单的Mybatis保存对象的例子 1@Test 2public void testSave() throws Exception { 3 //创建sessionFact ...

  4. 一张图轻松搞懂javascript event对象的clientX,offsetX,screenX,pageX区别

    总是会被javascript的event对象的clientX,offsetX,screenX,pageX 弄得头晕,于是决定做个图来区分一下(画得我手那个酸呀....) 先总结下区别: event.c ...

  5. 一张图搞懂Spring bean的完整生命周期

    一张图搞懂Spring bean的生命周期,从Spring容器启动到容器销毁bean的全过程,包括下面一系列的流程,了解这些流程对我们想在其中任何一个环节怎么操作bean的生成及修饰是非常有帮助的. ...

  6. 这一次搞懂Spring事务注解的解析

    前言 事务我们都知道是什么,而Spring事务就是在数据库之上利用AOP提供声明式事务和编程式事务帮助我们简化开发,解耦业务逻辑和系统逻辑.但是Spring事务原理是怎样?事务在方法间是如何传播的?为 ...

  7. 一张图带你搞懂Javascript原型链关系

    在某天,我听了一个老师的公开课,一张图搞懂了原型链. 老师花两天时间理解.整理的,他讲了两个小时我们当时就听懂了. 今天我把他整理出来,分享给大家.也让我自己巩固加深一下. 就是这张图: 为了更好的图 ...

  8. 一张图带你搞懂Node事件循环

    说一件重要的事儿:你还没关注公众号[前端印记],更多精彩内容等你探索-- 以下全文7000字,请在你思路清晰.精力充沛的时刻观看.保证你理解后很长时间忘不掉. Node事件循环 Node底层使用的语言 ...

  9. 40 张图带你搞懂 TCP 和 UDP

    前言 欢迎阅读「程序员cxuan」 的文章,从今往后,你就是我的读者了. 我的 github bestJavaer 已经收录此文章,目录在 https://github.com/crisxuan/be ...

随机推荐

  1. visual studio下载速度为0解决方法

    步骤: 一,更改网络设置 二,cmd刷新dns 一,更改网络设置 1,点开控制面板,打开网络和Internet 2,点击网络和共享中心 3,点击你连接的网络,那个是你连接的WIFI名字 4,点击属性 ...

  2. 学习PDO中的错误与错误处理模式

    在 PDO 的学习过程中,我们经常会在使用事务的时候加上 try...catch 来进行事务的回滚操作,但是大家有没有注意到默认情况下 PDO 是如何处理错误语句导致的数据库操作失败问题呢?今天,我们 ...

  3. PHP的可变变量与可变函数

    什么叫可变.在程序世界中,可变的当然是变量.常量在定义之后都是不可变的,在程序执行过程中,这个常量都是不能修改的.但是变量却不同,它们可以修改.那么可变变量和可变函数又是什么意思呢?很明显,就是用另一 ...

  4. dede新增字段调用方法

    各位在使用{dede:channel }标签的时候,难免会遇到因为现在字段不能满足业务需求,需要新增的情况(具体怎么新增字段自行百度). 但是新增的字段通过DEDE的标签是不能直接使用的,现在博主介绍 ...

  5. 获取系统版本,判断是windows还是Linux

    package com.foresee.zxpt.common.utils; import java.util.Properties; /** * 获取系统版本 * @author GZ * */ p ...

  6. Charles抓包工具断点修改返回内容

    在测试过程中,往往需要让服务器返回指定的内容,测试一些特殊情况.例如列表内容为空.数据异常的情况等.如果通过操作服务器配合构造相应的数据会比较麻烦,甚至不好构造数据.此时,可以使用Charles的断点 ...

  7. UOJ#33-[UR #2]树上GCD【长链剖分,根号分治】

    正题 题目链接:https://uoj.ac/problem/33 题目大意 给出\(n\)个点的一棵树 定义\(f(x,y)=gcd(\ dis(x,lca),dis(y,lca)\ )\). 对于 ...

  8. 使用AC自动机解决文章匹配多个候选词问题

    解决的问题 KMP算法用于单个字符串匹配,AC自动机用于文章中匹配多个候选词. 流程 第一步,先将候选词先建立前缀树. 第二步,以宽度优先遍历的方式把前缀树的每个节点设置fail指针, 头节点的fai ...

  9. Python自动化测试发送邮件太麻烦?!一起聊一聊 Python 发送邮件的3种方式

    1. 前言 发送邮件,我们在平时工作中经用到,做为测试人员,在自动化测试中用的也比较多,需要发送邮件给某领导 SMTP是Python默认的邮件模块,可以发送纯文本.富文本.HTML 等格式的邮件 今天 ...

  10. JVM堆内存泄露分析

      一.背景 公司有一个中间的系统A可以对接多个后端业务系统B,一个业务系统以一个Namespace代表, Namespace中包含多个FrameChannel(用holder保存),表示A连接到业务 ...