Version: Next

14. Spring 循环依赖

什么是循环依赖

  • 多个 Bean 之间相互依赖,形成了一个闭环
  • 例如:A 依赖 B、 B 依赖 C、C 依赖 A
  • 一般问 Spring 容器内部如何解决循环依赖问题,是 默认单例Bean中,属性互相引用的场景
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private C c;
}
@Component
public class C {
@Autowired
private A a;
}

构造器注入与 Set 注入对循环依赖的影响

在 Spring 个官方文档,依赖注入部分有写到

  • 如果用构造器注入,可能产生无法解决的循环依赖问题,抛出 BeanCurrentlyInCreationException
// 会出现这种代码
public Main {
public static void main(String[] args) {
new A(new B(new A(new (B))))..... // 写不完
}
}
  • 结论:只要循环依赖问题中,A 使用的注入方式是 set 注入,且为单例 singleton,就不会产生循环依赖问题
  • 默认的单例 singletion 场景支持循环依赖,不会报错;原型 prototype 场景不支持循环依赖,会报错

Spring 内部通过三级缓存解决循环依赖问题

提示

所谓的三级缓存,就是 Spring 内部定义的三个 Map

DefaultSingletonBeanRegistry

默认单例 Bean 缓存,下面有三个成员 Map

- Map<String, Object> `singletonObjects` = new ConcurrentHashMap<>(256);
- 俗称一级缓存:也叫单例池,存放已经经历完完整生命周期的 Bean 对象
- Map<String, Object> `earlySingletonObjects` = new HashMap(256);
- 俗称二级缓存:存放早期暴露出来的 Bean 对象,Bean 的生命周期未结束(属性还未注入完)
- Map<String, ObjectFactory<?>> `singletonFactories` = new HashMap<>(16);
- 俗称三级缓存:存放可以生成 Bean 的工厂
结论

只有单例的 Bean 会通过三级缓存提前暴露来解决循环依赖问题,而非单例的 Bean,每次从容器中获取的都是一个新的对象,会重新创建,所以非单例的 Bean 没有缓存,不会将其放到三级缓存中

Bean 如何在三级缓存中移动

实例化

内存中申请一块内存空间

初始化属性填充

注入成员属性

三个 Map 和四大方法、相关对象

  • 一级缓存:singletonObjects
  • 二级缓存:singletonFactories
  • 三级缓存:earlySingletonObjects
  • getSingleton():获取单例 Bean 的方法,如果拿不到才去创建
  • doCreateBean():创建 Bean
  • populateBean():填充 Bean 的属性,会被 doCreate() 调用
  • addSingletton():将填充好属性的 Bean 从三级到一级逐级添加到缓存中的方法

A / B 两个对象再三级缓存中的迁移说明

  • 一级缓存:singletonObjects 存放的是已经初始化的 Bean
  • 二级缓存:earlySingletonObjects 存放的是实例化,但是为初始化的 Bean
  • 三级缓存:singletionFactories 存放的是 FactoryBean。假设 A 类实现了 FactoryBean 那么依赖注入的时候不是 A 类,而是 A 类产生的 Bean

实例化 A 的过程中需要实例化 B

  1. A 创建过程中需要 B,于是 A 把自己放到三级缓存中,转而去实例化 B
  2. B 实例化时发现需要 A,于是 B 先查看一级缓存,没有A;再检查二级缓存,没有 A;再检查三级缓存,发现了 A,于是把三级缓存中的 A 拿到二级缓存中,并删除三季缓存中的 A
  3. B 顺利初始化完毕,将自己放到一级缓存中 (此时 B 中的 A 依然是创建中状态)。接着,回来继续创建 A,此时 B 已经创建结束,A 从一级缓存中找到 B,完成创建,最后 A 把自己从二级缓存放到一级缓存中

在 A / B 的构造方法中写一句 sout,通过何时打印出两句 sout,来追踪 Spring 初始化单例 Bean 的代码位置

  1. context.getBean("a", A.class)
  2. ClassPathXmlApplicationContext 类, refresh() 方法
  3. AbstractApplicationContext 类 finishBeanFactoryInitialization(beanFactory) 方法 (初始化所有依然存在的,非懒加载单例 Bean)
  4. AbstractApplicationContext 类,preInstantiateSingletons() 方法(初始化所有依然存在的,非懒加载单例 Bean)
  5. AbstractionBeanFactory 类 doGetBean()
  6. doGetBean() 调用 getSingleton(beanName)
  7. getSingleton 方法调用 singletonObject.get(beanName) 一级缓存中找,此时既没有容器,也没有对象,因此后续要走 doCreateBeanpopulateBeanaddSingletton 这三个方法
  8. doCreateBean 中,addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) 方法中,singletonFactories.put(beanName, singletonFactory) 把创建好的 Bean 放到三级缓存,即 a 已经放入了三级缓存
  9. 调用 populateBean(beanName, mbd, instanceWrapper),其中属性 PropertyValues 记录当前 Bean 需要填充的属性,发现需要填充一个 B 类的对象,但是 B 的对象还不存在,触发 resolveValueIfNecessary(Object argName, Object value) 方法,触发 resolveReference 方法处理引用
  10. 触发 doGetBean() -> getSingleton(beanName).... -> 去一级缓存中找找不到 b -> doCreateBean -> ... 直到 b 被创建,也被放到三级缓存
此时状态

此时 a / b 两个 bean 都在三级缓存中

  1. b 调用 populateBean() 方法尝试填充属性,发现需要一个 a,调用 getSingleton 去找 a,搜索一级缓存发现不存在 a,并且此时 a 正处于被创建的过程中(因为在三级缓存中),满足这两个条件,尝试从二级缓存 earlySingletonObjects 获取 a如果获取结果为 null 说明二级缓存也没有,就去 三级缓存 singletonFactories 中找到 a,并且把 a 移动到二级缓存
此时状态
  • a 在二级缓存中,它刚刚从三级缓存中被删除,添加到二级缓存中
  • b 还在三级缓存中
  1. 把 a 填充到 b 的成员属性 a 中,再次来到 getSingleton() 方法,在一级缓存中找 b,没找到,且 allowEarlyBean 为 false,不去三级缓存找,继续执行到 addSingleton(beanName, singletonObject) 方法,把 b 加入到一级缓存,同时无脑移除二级、三级缓存中可能存在的任何 b Bean
此时状态
  • a 在二级缓存中
  • b 在一级缓存中,它从三级缓存直接蹦跶到了一级缓存
  1. 此时 a 还没有创建完成,在一级缓存中搜素 a,没有,且 a 正在创建过程中,则去二级缓存找 a,找到了
  2. a 调用 populateBean(),填充属性,发现需要一个 b,调用 getSingleton 去找 b,在一级缓存中发现了 b,把 b 设置到自己的成员属性 b 上
  3. a 调用 addSingleton() 方法,把自己 设置到一级缓存,同时删除二级、三级缓存中任何 a Bean
此时状态
  • a 从二级缓存进入一级缓存
  • a / b 都进入一级缓存,完事
  1. getBean() 方法执行完毕,返回 A 类的实例化对象 a
  2. 执行 B 类的 getBean(),此时可以直接从一级缓存中获取到 b