Loading

Spring 中 Bean 的循环依赖及解决方案

什么是循环依赖

A 直接或间接依赖 B 的同时 B 又间接或直接依赖 A,此时我们可以称 A 和 B 之间存在循环依赖关系。在使用 Spring 的过程中应该尽量避免循环引用关系的出现。

生命周期简述

在阅读下面的样例之前,需要先了解一下 Spring 中 bean 的生命周期,简单来说 bean 的生命周期分为:

  1. 实例化
  2. 属性填充 (属性注入发生在这个阶段)
  3. 初始化
  4. 使用阶段
  5. 销毁

其中的初始化阶段又可以细分为:

  1. 初始化前置处理 (各种 Aware 通常在这个阶段调用 set 方法, 也可以自定义 BeanPostProcessor 来替换已经实例化且完成属性填充的 bean)
  2. 初始化处理 (可以自定义 bean 初始化代码)
  3. 初始化后置处理 (正常情况下 AOP proxy 发生在这个阶段, 后面会讲提前发生 proxy 的场景, 这里也可以自定义 BeanPostProcessor 来替换已经实例化、完成属性填充且基本初始化完毕的 bean)

Spring 中 IoC 的初始化就是围绕着 bean 的生命周期流程来完成的,bean 的生命周期也是 Spring 中的核心内容。

循环依赖样例分析

通过以下样例可以更进一步的认识什么是循环依赖以及如何解决循环依赖

一,实例化阶段依赖

这里的实例化依赖是指:A 在实例化的阶段中依赖 B, 同时 B 又依赖 A

示例代码

class InstDepApplication

// c1 构造参数依赖 c2
open class C1(private val c2: C2)

// c2 构造参数依赖 c1
open class C2(private val c1: C1)

// 或者 c2 属性依赖 c1, 此时和构造参数依赖 c1 的效果是一样的
//open class C2 {
//    @Autowired
//    private lateinit var c1: C1
//}

fun main() {
    val applicationContext = AnnotationConfigApplicationContext(
        InstDepApplication::class.java,
        C1::class.java,
        C2::class.java,
    )
}

运行结果: 应用因循环依赖启动失败
流程分析:

  1. 将 c1 记录到当前创建流程中 并开始创建 c1
  2. 实例化 c1 时,C1 的构造参数依赖 c2,因此会在 IoC 中 获取依赖项 c2
  3. 在查找后发现 c2 还未创建,则尝试创建 c2,即重复第一步:将 c2 记录到当前创建流程中 并开始创建 c2
  4. 实例化 c2 时,C2 的构造参数依赖 c1,因此会在 IoC 中 获取依赖项 c1
  5. 在查找后发现 c1 还未创建,则尝试创建 c1,但是再次 将 c1 记录到当前创建流程中 时,因为 c1 已经存在于当前创建流程中,导致新记录添加失败,所以判定当前创建流程 存在无法处理的循环依赖关系,原因是:正在创建过程中的 bean 又需要进行创建,从而导致应用启动失败。

解决方案

此时只能通过 @Lazy 注解为依赖项生成代理对象间接获取被依赖的 bean

open class C2(@Lazy private val c1: C1)

实际上在任意一处依赖项上添加 @Lazy 都可以解决循环依赖问题

@Lazy 的原理和注意事项

以上面的代码为例,对 C2 的依赖型 C1 添加 @Lazy 注解后,在 C2 的实例化阶段不会直接从 IoC 中获取依赖项 c1,取而代之的是先创建一个 C1 的代理对象 cp1 来作为 C2 的构造参数完成 c2 的实例化。
cp1 是通过 Proxy 或 CGLIB 生成的 Class 且利用反射来创建的对象,它只是一个临时对象,作用是延迟从 IoC 中获取 c1 的时机:当 cp1 被再次访问时才会触发从 IoC 中获取 c1 的动作。

空参实例化 C2 是通过反射来实现的:

org.springframework.objenesis.instantiator.sun.SunReflectionFactoryInstantiator
sun.reflect.ReflectionFactory

生成 cp1 的相关代码:

protected Object buildLazyResourceProxy(LookupElement element, @Nullable String requestingBeanName) {
		TargetSource ts = new TargetSource() {
			@Override
			public Class<?> getTargetClass() {
				return element.lookupType;
			}
			@Override
			public Object getTarget() {
				return getResource(element, requestingBeanName);
			}
		};

		ProxyFactory pf = new ProxyFactory();
		pf.setTargetSource(ts);
		if (element.lookupType.isInterface()) {
			pf.addInterface(element.lookupType);
		}
		ClassLoader classLoader = (this.beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory ?
				configurableBeanFactory.getBeanClassLoader() : null);
		return pf.getProxy(classLoader);
	}

这个方案可以解决 C2 构造参数中依赖正在创建过程中但还为实例化的 bean 的问题,但是此时存在一个前提是:在 c1 放入 IoC 之前不能访问 @Lazy 为其生成的代理对象 cp1,否则会触发对 c1 的获取,导致继续创建 c1,从而重复上述的失败流程。所以不要在 c2 实例化完成后的初始化阶段访问它的 c1 属性,反例:

class InstDepApplication

open class C1(val c2: C2)

open class C2(@Lazy val c1: C1) {
    @PostConstruct
    fun postConstruct() {
        println(c1)
    }
}

fun main() {
    val applicationContext = AnnotationConfigApplicationContext(
        InstDepApplication::class.java,
        C1::class.java,
        C2::class.java,
    )
    println(applicationContext.getBean(C2::class.java).c1)
}

InitializingBeaninit-method@PostConstruct 同理

二,初始化阶段依赖

这里的初始化阶段依赖是指:A 在实例化完成后的阶段(包括属性注入阶段和初始化阶段)中依赖 B, 同时 B 又依赖 A。
通常初始化依赖不属于无法处理的循环依赖关系,因为在 spring 中默认会通过三级缓存机制来调解循环依赖关系。

示例代码

class InstDepApplication

// c1 构造参数依赖 c2
open class C1() {
    @Autowired
    lateinit var c2: C2
}

// c2 构造参数依赖 c1
open class C2(val c1: C1)

// 或者 c2 属性依赖 c1, 此时和构造参数依赖 c1 的效果是一样的
//open class C2 {
//    @Autowired
//    private lateinit var c1: C1
//}

fun main() {
    val applicationContext = AnnotationConfigApplicationContext(
        InstDepApplication::class.java,
        C1::class.java,
        C2::class.java,
    )
    println(applicationContext.getBean(C2::class.java).c1)
}

运行结果: 应用启动成功
流程分析:

  1. 将 c1 记录到当前创建流程中 并开始创建 c1
  2. 实例化 c1 后,将 c1 的 singletonFactory 放入 IoC 的第三级缓存(前提是 IoC 的 allowCircularReferences 为 true,默认为 true)
  3. 接下来为 c1 进行属性填充,因为 c1 的属性注入依赖 c2,因此会在 IoC 中 获取依赖项 c2
  4. 在查找后发现 c2 还未创建,则尝试创建 c2,即重复第一步:将 c2 记录到当前创建流程中 并开始创建 c2
  5. 实例化 c2 时,C2 的构造参数依赖 c1,因此会在 IoC 中 获取依赖项 c1,此时将从 IoC 的第三级缓存中获取到 c1 的 singletonFactory,从而触发 singletonFactory 的 getEarlyBeanReference,最终将三级缓存中 c1 的 singletonFactory 提升为 earlySingletonObject 并作为参数完成 C2 的实例化
  6. 随后 c2 完成创建,则 c1 回到属性注入阶段继续完成创建,最终 IoC 初始化完成,应用成功启动。

Q:为什么第三级缓存中存放的是 singletonFactory 而不是刚实例化完成的 c1?
A:因为在 c1 的初始化阶段中 c1 所指向的对象是可以被修改掉的,所以 Spring 的正常预期是在 c1 初始化完成后再执行 wrapIfNecessary 进行 AOP 代理(如果过早的进行 AOP 会出现 AOP 内部引用的 c1 和最终要添加到 IoC 中的 c1 不是同一个对象的情况),但是如果有其他 bean 在 c1 实例化之后且初始化之前就需要访问 c1 的话,就需要将 wrapIfNecessary 这个操作提前(在这之后 c1 的初始化不允许再修改 c1 所指向的对象,否则应用将启动失败),所以第三级缓存中存放的 singletonFactory 其实就是对 wrapIfNecessary 的调用

Q:为什么二级缓存存放的对象叫做 earlySingletonObjects?
A:因为他们是在执行初始化阶段完成前就被 AOP 的对象,它们后续还需要继续执行未完成的初始化。

Q:第二级缓存中的对象什么时候提升到第一级,那些没有被提前访问的 singletonFactory 呢?
A:在 bean 的创建方法执行结束之后,其返回值会被添加到缓存的第一级,同时清空该 bean 对应的 beanName 在第二级和第三级中存放的对象。因此它们并不会被直接提升到第一级,但是第二级缓存中的对象会被用来检验 bean 在初始化阶段是否发生的对象替换。

IoC 的三级缓存:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// Quick check for existing instance without full singleton lock
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				synchronized (this.singletonObjects) {
					// Consistent creation of early reference within full singleton lock
					singletonObject = this.singletonObjects.get(beanName);
					if (singletonObject == null) {
						singletonObject = this.earlySingletonObjects.get(beanName);
						if (singletonObject == null) {
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
							if (singletonFactory != null) {
								singletonObject = singletonFactory.getObject();
								this.earlySingletonObjects.put(beanName, singletonObject);
								this.singletonFactories.remove(beanName);
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}

解决方案

spring 中默认会将 allowCircularReferences 设置为 true 来开启三级缓存机制调解循环依赖关系。

注意事项

通过上面的流程分析我们知道 earlySingletonObject 是不可以在后续的初始化阶段被修改所指向对象的,否则该单例 bean 就会出现两份不同的对象,反例:

class InstDepApplication : BeanPostProcessor {
    override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
        // 此时 c1 已经被注入到 c2 中
        if (beanName == "c1") {
            // c1 所指向的 对象被修改了
            return C1().also {
                it.c2 = (bean as C1).c2
            }
        }
        return bean
    }
}

// c1 构造参数依赖 c2
open class C1() {
    @Autowired
    lateinit var c2: C2
}

// c2 构造参数依赖 c1
open class C2(val c1: C1)

// 或者 c2 属性依赖 c1, 此时和构造参数依赖 c1 的效果是一样的
//open class C2 {
//    @Autowired
//    private lateinit var c1: C1
//}

fun main() {
    val applicationContext = AnnotationConfigApplicationContext(
        InstDepApplication::class.java,
        C1::class.java,
        C2::class.java,
    )
    println(applicationContext.getBean(C2::class.java).c1)
}

自引用依赖

自引用依赖指的是一个 bean 的构造参数或属性中依赖自己, 其本质上还是实例化阶段依赖或初始化阶段依赖, 这种情况最常发生在一个 bean 中的事务方法调用时,为了解决直接调用导致的事务失效问题, 需要使用被事务 aop 代理之后的对象.

事务失效的反例:

@Service
class XService {
    fun query() {
        // 直接调用 @Transactional 方法, 事务不会生效
        update()
    }

    @Transactional
    fun update() {}
}

此时需要通过被 aop 处理后的 bean 来调用事务方法(此时又会出现循环依赖问题):

@Component
class SelfReferenceComponent(
    // ERROR: Circular Reference
    val self: SelfReferenceComponent
) {
//    @Autowired
//    private lateinit var self: SelfReferenceComponent

    fun query() {
        // 通过被代理后的 bean 调用 @Transactional 方法, 事务正常执行
        self.update()
    }

    @Transactional
    fun update() {}
}

其解决方案就是通过 @Lazy 注解打破循环关系

posted @ 2024-05-09 23:47  xtyuns  阅读(212)  评论(0编辑  收藏  举报