首页 > 其他分享 >Spring 中 bean 的循环依赖

Spring 中 bean 的循环依赖

时间:2024-05-09 23:56:40浏览次数:28  
标签:依赖 Spring bean C2 c2 c1 C1 class

什么是循环依赖

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)
}

标签:依赖,Spring,bean,C2,c2,c1,C1,class
From: https://www.cnblogs.com/xtyuns/p/18181244

相关文章

  • Springboot项目镜像制作&传递环境变量、设置hostname、动态设置JVM参数、cmd&entrypoi
    实现制作一个springboot的镜像,并且可以传递环境变量实现动态JVM参数和端口。0.准备&cmd、entrypoint区别1.准备springboot项目一个简单的springboot项目,默认启动8001端口,里面只有一个接口。xxx%curllocalhost:8081indexdocker环境2.CMD、entrypoint区......
  • springboot3.2.3如何集成swagger
    在SpringBoot中集成Swagger可以方便地生成API文档并进行接口测试。要在SpringBoot3.2.3中集成Swagger,你可以按照以下步骤进行操作:1.添加Swagger依赖到pom.xml文件中:点击查看代码<dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter<......
  • openfeign接口Springboot启动Bean报错未找到Singleton bean creation not allowed whi
    检查步骤检查springboot启动类是否标注@EnableFeignClients注解,未标注该注解会导致无法注入bean检查远程调用模块是否标注注解@FeignClient检查@FeignClient注解中是否写了正确的微服务名称(区分大小写)检查@FeignClient注解中标识的微服务是否启动​​原因:此处接......
  • springboot+vue项目
    1MyBatisPlus的分页插件是怎么生效的?体现在哪里?PaginationInnerInterceptor是通过拦截数据库操作来实现分页功能的。 MyBatisPlus的分页插件PaginationInnerInterceptor是通过拦截数据库操作来实现分页功能的。它的工作原理如下:配置分页插件:在你的SpringBoot应用......
  • SpringBoot随手笔记
    SpringBoot随手笔记0关于火狐浏览器什么时候会发出http请求的说明在抓包的情况下(按下F12后的模式),不管是刷新页面还是在浏览器地址栏回车,该页面中的图片都会发出http请求;但如果不是抓包的模式下,如果访问的页面和上一次访问的页面相同(地址栏的地址没有更改),不管是刷新页面还......
  • springboot seata 全局捕获异常失效
    问题:Springboot使用@ControllerAdvice或@RestControllerAdvice全局捕获异常时,捕获不到自己抛出的相应异常首先看一下全局异常组件有么有被扫描到如何查看,很简单只需要写一段类加载打印代码,如下 如果启动时,打印了你写的字符串就说明时烧苗到了 这就说明是其他的问题了,那就......
  • Springboot项目的jar包的运行方式以及使用yum安装java后忘记了位置
    SpringBoot项目打包后的jar的部署方式这里我写了五种部署方式1.直接启动java-jarxxx.jar这种方式就只适合自己在测试时用一下,关闭会话就能停止运行属实是方便。2.后台启动java-jarxxx.jar&在后台静默启动,同样关闭会话也会停止,优点是和上面一样,日志是打印在窗口的3......
  • Springboot - [06] yaml语法讲解
    Yaml是一种标记语言,Yaml也不是一种标记语言。 一、yaml写法示例application.yaml#普通的key-valuename:harleyserver.port:8081#对象student:name:harleyage:25#对象的行内写法student:{name:harley,age:25}#数组pets:-cat-......
  • SpringBoot - [04] 自动装配原理
    题记部分   SpringBoot的自动装配(Auto-Configuration)原理是其简化Spring应用开发的关键特性之一,它能自动配置Spring框架及第三方库,极大地减少了手动配置的工作量。以下是SpringBoot自动装配的核心原理和步骤:(1)条件化配置(ConditionalConfiguration):  SpringBoot利用Spr......
  • Springboot - [05] 彩蛋~
    题记部分  彩蛋一:如何更换Springboot启动时的logo(1)访问https://www.bootschool.net/ascii-art/search,搜索到佛祖的ASCII艺术字(图)集(2)将内容拷贝到src/main/resources下的文件中,文件名称为banner.txt(3)启动SpringBoot的应用  彩蛋二:  彩蛋三:   —要......