Spring 如何解决循环依赖
1. 前言
1.1. 什么是循环依赖?
循环依赖:一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。
-
第一种情况:自己依赖自己的直接依赖
-
第二种情况:两个对象之间的直接依赖
-
第三种情况:多个对象之间的间接依赖
1.2. Spring 创建 Bean 主要流程
1.2.1. 实例化 Bean
// org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#doCreateBean
instanceWrapper = createBeanInstance(beanName, mbd, args);
主要是通过反射调用默认构造函数创建 Bean 实例,此时 Bean 的属性都还是默认值 null。被注解 @Bean 标记的方法就是此阶段被调用的。
1.2.2. 填充 Bean 属性
// org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#doCreateBean
populateBean(beanName, mbd, instanceWrapper);
这一步主要是对 Bean 的依赖属性进行填充,对 @Value、@Autowired、@Resource 注解标注的属性注入对象引用。
1.2.3. 调用 Bean 初始化方法
// org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#doCreateBean
exposedObject = initializeBean(beanName, exposedObject, mbd);
调用配置指定中的 init 方法,例如,如果 xml 文件指定 Bean 的 init-method 方法或注解 @Bean(initMethod = "initMethod")
指定的方法。
1.3. BeanPostProcessor 接口拓展点
在 Bean 创建的流程中 Spring 提供了多个 BeanPostProcessor 接口方便开发者对 Bean 进行自定义调整和加工。
有以下几种 BeanPostProcessor 接口比较常用:
-
postProcessMergedBeanDefinition
:可对 BeanDefinition 添加额外的自定义配置; -
getEarlyBeanReference
:返回早期暴露的 Bean 引用,一个典型的例子是循环依赖时如果有动态代理,需要在此先返回代理实例; -
postProcessAfterInstantiation
:在 populateBean 前用户可以手动注入一些属性; -
postProcessProperties
:对属性进行注入,例如配置文件加密信息在此解密后注入; -
postProcessBeforeInitialization
:属性注入后的一些额外操作; -
postProcessAfterInitialization
:实例完成创建的最后一步,这里也是一些 BPP 进行 AOP 代理的时机。
最后,对 Bean 的生命流程进行一个流程图的总结:
Spring 的动态代理(AOP)就是通过 BeanPostProcessor 实现的(图中的 3.4 步),其中 AbstractAutoProxyCreator 是十分典型的自动代理类,它实现了 SmartInstantiationAwareBeanPostProcessor 接口,并重写了 getEarlyBeanReference 和 postProcessAfterInitialization 两个方法实现代理的逻辑,这样完成对原始 Bean 进行增强,生成新 Bean 对象,将增强后的新 Bean 对象注入到属性依赖中。
2. Spring 解决循环依赖的流程分析
注意,我们需要在 Spring 里面使能运行循环依赖的配置项,这样才能支持循环依赖:
spring.main.allow-circular-references=true
2.1. 三级缓存
// org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// ...
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// ...
}
三级缓存:
-
singletonObjects:一级缓存
主要存放的是已经完成实例化、属性填充和初始化所有步骤的单例 Bean 实例,这样的 Bean 能够直接提供给用户使用,我们称之为终态 Bean 或叫成熟 Bean。
-
earlySingletonObjects:二级缓存
主要存放的已经完成初始化,但属性还没自动赋值的 Bean,这些 Bean 还不能提供用户使用,只是用于提前暴露的 Bean 实例,我们把这样的 Bean 称之为临时 Bean 或早期的 Bean(半成品 Bean)。
-
singletonFactories:三级缓存
存放的是 ObjectFactory 的匿名内部类实例,调用 ObjectFactory.getObject() 最终会调用 getEarlyBeanReference 方法,该方法可以获取提前暴露的单例 Bean 引用。
2.2. 流程分析
Spring 通过三级缓存解决了循环依赖,其中,一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象 earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。
当 A、B 两个类发生循环引用时,在 A 完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果 A 被 AOP 代理,那么,通过这个工厂获取到的就是 A 代理后的对象,如果 A 没有被 AOP 代理,那么,这个工厂获取到的就是 A 实例化的对象。
当 A 进行属性注入时,会去创建 B,同时,B 又依赖了 A,所以,创建 B 的同时又会去调用 getBean(a) 来获取需要的依赖,此时的 getBean(a) 会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到 B 中。紧接着 B 会走完它的生命周期流程,包括初始化、后置处理器等。当 B 创建完后,会将 B 再注入到 A 中,此时, A 再完成它的整个生命周期。
通过上述流程,可以看出 bean 都是需要先可以被实例化才可以的,所以,这也就是为什么构造器依赖可能会失败的原因。
例如,Bean A 的构造器依赖 B,而实例化 A 需要先调用 A 的构造函数,发现依赖 B,那么,需要再去初始化 B,但是,B 也依赖 A,不管 B 是通过构造器注入还是 setter 注入,此时,由于 A 没有被实例化,没有放入三级缓存,所以, B 无法被初始化,所以,spring 会直接报错。反之,如果 A 通过 setter 注入的话,那么,则可以通过构造函数先实例化,放入缓存,然后再填充属性,这样的话不管 B 是通过 setter 还是构造器注入 A,都能在缓存中获取到,于是可以初始化。
2.2.1. Spring 如何解决循环依赖
在 Spring 中,只有同时满足以下两点才能解决循环依赖的问题:
-
依赖的 Bean 必须都是单例
-
依赖注入的方式,必须不全是构造器注入,且 beanName 字母序在前的不能是构造器注入
spring 的 bean 加载顺序:默认情况下,是按照文件完整路径递归查找的,按路径 + 文件名排序,排在前面的先加载。
2.2.1.1. 为什么必须是单例
如果循环依赖的 Bean 是原型模式,会直接抛错,其源码如下:
// org/springframework/beans/factory/support/AbstractBeanFactory.java
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
// ...
// Fail if we're already creating this bean instance:
// We're assumably within a circular reference.
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// ...
}
2.2.1.2. 为什么无法支持原型对象
因为原型模式都需要创建新的对象,不能用以前的对象。
如果 Bean A 和 Bean B 都是原型模式的话,那么,
-
创建 A1 需要创建一个 B1;
-
创建 B1 的时候要创建一个 A2;
-
创建 A2 又要创建一个 B2;
-
创建 B2 又要创建一个 A3;
-
创建 A3 又要创建一个 B3
-
...
就会陷入死循环。
如果是单例的话,创建 A 需要创建 B,而创建的 B 需要的还是之前的个 A。
2.2.1.3. 为什么不能全是构造器注入
在 Spring 中创建 Bean 分三步:
-
实例化:createBeanInstance,就是 new 了一个对象
-
属性注入:populateBean, 就是 set 了一些属性值
-
初始化:initializeBean,执行一些 aware 接口中的方法,initMethod、AOP 代理等
如果全是构造器注入,例如,A的构造器 A(B b),那就表明在 new 的时候,就需要得到 B,此时需要 new B(A a) 。
但是,B 也是要在构造的时候注入 A ,即 B(A a),这时候, B 需要在一个 map 中找到不完整的 A ,就会发现找不到,从而导致 Bean 创建失败。
2.2.1.4. 为什么循环依赖需要三级缓存,二级不够吗
思考:如果在实例化 Bean A 之后,在二级 map 里面保存这个 A,然后继续属性注入。发现 A 依赖 B,所以要创建 Bean B,这时候, B 就能从二级缓存得到 A ,完成 B 的建立之后, Bean A 似乎也能完成实例化。
很明显,如果仅仅只是为了解决循环依赖,二级缓存够了,根本就不必要三级缓存。但是,如果我们希望对添加到三级缓存中的实例对象进行增强(例如,AOP),直接用实例对象是行不通的。
但是我们都知道对象如果有代理的话,那么,我们希望直接拿到的是代理对象。
也就是说如果 A 需要被代理,那么, B 依赖的 A 是已经被代理的 A,所以,我们不能返回 A 给 B,而是返回 A 的代理对象给 B。
其核心的代码实现如下:
// org/springframework/beans/factory/support/AbstractBeanFactory.java
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
// ...
// 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) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// ...
}
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
// 判断是否有后置处理器
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
// Bean需要被AOP代理
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
// ...
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args); // 创建单例Bean
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// ...
}
三级工厂的作用就是判断这个对象是否需要代理,如果否则直接返回,如果是则返回代理对象。
2.2.1.5. 为什么代理对象没有放在二级缓存中
通常代理对象的生成是基于后置处理器,是在被代理的对象初始化后期调用生成的,所以,如果我们提早代理了其实是违背了 Bean 定义的生命周期。所以, Spring 先在一个三级缓存放置一个工厂,如果产生循环依赖,那么,就调用这个工厂提早得到代理对象。
如果没产生依赖,这个工厂根本不会被调用,所以,Bean 的生命周期就是对的。
3. 不同的循环依赖场景分析
spring 中出现循环依赖主要有以下场景:
3.1. 单例的 setter 注入(启动成功)
- EmployeeService
public class EmployeeService {
@Autowired
private CompanyService companyService;
@Override
public String toString() {
return "EmployeeService";
}
}
- CompanyService
@Service
public class CompanyService {
@Autowired
private EmployeeService employeeService;
@Override
public String toString() {
return "CompanyService";
}
}
这是一个经典的循环依赖,但是它能正常运行,得益于 spring 的内部机制,让我们根本无法感知它有问题,因为 spring 默默帮我们解决了。
spring内部有三级缓存:
-
singletonObjects
:一级缓存,用于保存实例化、注入、初始化完成的bean实例 -
earlySingletonObjects
:二级缓存,用于保存实例化完成的bean实例 -
singletonFactories
:三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。
spring 解决循环依赖的流程如下:
3.2. 多例的 setter 注入(启动成功)
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
public void test1() {
}
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {
}
}
这种情况,spring容器可以启动成功。
spring IOC 容器的初始化流程可以看出:非抽象、单例 并且非懒加载的类才能被提前初始 bean。
// org/springframework/beans/factory/support/DefaultListableBeanFactory.java
public void preInstantiateSingletons() throws BeansException {
// ..
// Trigger initialization of all non-lazy singleton beans...
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) {
getBean(beanName);
}
}
else {
getBean(beanName);
}
}
}
// ...
}
而多实例的类,即 SCOPE_PROTOTYPE 类型,不会被提前初始化bean,所以程序能够正常启动。
注意,如果这个多实例 Bean 被其他的单例 Bean 依赖了,就会导致启动失败。
3.3. 构造器注入(启动失败)
@Service
publicclass TestService1 {
public TestService1(TestService2 testService2) {
}
}
@Service
publicclass TestService2 {
public TestService2(TestService1 testService1) {
}
}
这种场景会导致启动失败。
出构造器注入没能添加到三级缓存,也没有使用缓存,所以也无法解决循环依赖问题。
3.4. 单例的代理对象setter注入(可能会启动失败)
这种注入方式其实也比较常用,例如,使用 @Async
注解的场景,会通过 AOP 自动生成代理对象。
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
@Async
public void test1() {
}
}
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {
}
}
这种情况下的循环依赖,spring 就无法解决,就会导致启动报错。
它的启动过程如下:
bean初始化完成之后,后面还有一步去检查:第二级缓存 和 原始对象 是否相等。
如果这时候把TestService1改个名字,改成 TestService6,其他的都不变,就可以解决启动依赖报错的问题了:
@Service
publicclass TestService6 {
@Autowired
private TestService2 testService2;
@Async
public void test1() {
}
}
这种情况下,TestService2 会比 TestService6 先加载,这种情况下就不会有问题
这种情况下,testService6 中其实第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。
3.5. DependsOn 循环依赖(启动失败)
@DependsOn(value = "testService2")
@Service
publicclass TestService1 {
@Autowired
private TestService2 testService2;
}
@DependsOn(value = "testService1")
@Service
publicclass TestService2 {
@Autowired
private TestService1 testService1;
}
这种情况会导致启动失败。
其中,相关代码实现如下:
// org/springframework/beans/factory/support/AbstractBeanFactory.java
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
// ...
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
}
registerDependentBean(dep, beanName);
try {
getBean(dep);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
}
}
}
// ...
}
IOC 容器的启动过程中,它会检查 dependsOn 的实例有没有循环依赖,如果有循环依赖则抛异常。
4. 出现循环依赖如何解决
-
生成代理对象产生的循环依赖
-
使用 @Lazy 注解,延迟加载;
-
使用 @DependsOn 注解,指定加载先后关系;
-
修改文件名称,改变循环依赖类的加载顺序。
-
-
使用 @DependsOn 产生的循环依赖
- 找到 @DependsOn 注解循环依赖的地方,迫使它不循环依赖就可以解决问题。
-
多例循环依赖
- 把 bean 改成单例。
-
构造器循环依赖
- 使用 @Lazy 注解。
5. 总结
Spring 处理循环依赖的方式:
-
有构造器注入,不一定会产生问题,具体得看是否都是构造器注和 BeanName 的字母序;
-
如果单纯为了打破循环依赖,不需要三级缓存,两级就够了;
-
三级缓存是否为延迟代理的创建,尽量不打破 Bean 的生命周期。
参考: