高级java每日一道面试题-2024年10月7日-框架篇[springboot篇]-springboot如何处理循环依赖的问题?
如果有遗漏,评论区告诉我进行补充
面试官: springboot如何处理循环依赖的问题?
我回答:
循环依赖的概念
当两个或多个 Bean 相互依赖对方,就形成了循环依赖。例如,Bean A 依赖 Bean B,同时 Bean B 又依赖 Bean A。这会导致应用程序无法正确地初始化和运行,因为Spring Boot无法处理这种循环依赖关系。
Spring Boot 处理循环依赖的机制
Spring容器在启动时,会扫描配置文件(如applicationContext.xml)或注解定义的Bean,并尝试创建这些Bean的实例。在这个过程中,Spring会跟踪哪些Bean正在被创建,以便检测循环依赖。Spring通过一个名为“DefaultSingletonBeanRegistry”的类来跟踪单例Bean的创建状态,并使用三个主要的缓存来管理Bean的创建过程:
三级缓存机制
- 一级缓存:singletonObjects
- 这是一个
ConcurrentHashMap
,用于存放完全初始化好的单例 Bean。当一个 Bean 完成创建和初始化(包括属性注入等所有步骤)后,就会被放入这个缓存中。
- 这是一个
- 二级缓存:earlySingletonObjects
- 也是一个
ConcurrentHashMap
。当一个 Bean 正在创建过程中(还未完全初始化),但是已经被创建了实例(通过构造函数创建),就会被放入这个缓存。这个缓存主要是为了解决循环依赖中的半成品 Bean 的暴露问题。
- 也是一个
- 三级缓存:singletonFactories
- 这是一个
ConcurrentHashMap
,存放的是ObjectFactory
对象。当一个 Bean 开始创建时,会将创建这个 Bean 的ObjectFactory
放入这个缓存。ObjectFactory
是一个函数式接口,它的作用是在需要的时候创建 Bean 实例。
- 这是一个
创建过程中的循环依赖处理
- Bean A 的创建过程
- 当容器开始创建 Bean A 时,首先会将 Bean A 的创建工厂(
ObjectFactory
)放入三级缓存singletonFactories
。 - 然后进行 Bean A 的实例化,通过构造函数创建出 Bean A 的实例,但此时 Bean A 还未完成属性注入等初始化操作。
- 当进行 Bean A 的属性注入时,发现依赖 Bean B。
- 当容器开始创建 Bean A 时,首先会将 Bean A 的创建工厂(
- Bean B 的创建过程
- 容器开始创建 Bean B,同样先将 Bean B 的创建工厂放入三级缓存
singletonFactories
。 - 实例化 Bean B,在对 Bean B 进行属性注入时发现依赖 Bean A。
- 此时容器会在三级缓存中查找 Bean A 的创建工厂,通过这个工厂得到 Bean A 的早期实例(半成品实例,还未完全初始化),将这个早期实例放入二级缓存
earlySingletonObjects
,并从三级缓存中移除 Bean A 的创建工厂。 - Bean B 完成属性注入(其中包含了注入 Bean A 的早期实例)和其他初始化操作,成为一个完全初始化的 Bean,放入一级缓存
singletonObjects
。
- 容器开始创建 Bean B,同样先将 Bean B 的创建工厂放入三级缓存
- Bean A 继续创建过程
- 由于 Bean B 已经完成创建并注入到 Bean A 中,Bean A 可以继续完成自己的属性注入(此时注入的 Bean B 是完全初始化的)和其他初始化操作,成为一个完全初始化的 Bean,放入一级缓存
singletonObjects
。
- 由于 Bean B 已经完成创建并注入到 Bean A 中,Bean A 可以继续完成自己的属性注入(此时注入的 Bean B 是完全初始化的)和其他初始化操作,成为一个完全初始化的 Bean,放入一级缓存
示例代码说明
以下是一个简单的示例代码来演示循环依赖的情况:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
public class ClassA {
private ClassB classB;
@Autowired
public ClassA(ClassB classB) {
this.classB = classB;
}
}
@Component
public class ClassB {
private ClassA classA;
// 使用@Lazy注解可以延迟加载,避免在创建ClassB时立即触发循环依赖的初始化问题
@Autowired
public ClassB(@Lazy ClassA classA) {
this.classA = classA;
}
}
在上述代码中,ClassA
依赖ClassB
,ClassB
又依赖ClassA
。通过 Spring Boot 的循环依赖处理机制,即使存在这种循环依赖关系,也能够正确地创建和初始化这两个 Bean。
需要注意的是,虽然 Spring Boot 能够处理循环依赖,但循环依赖通常是一种不良的设计模式,可能会导致代码难以理解和维护,在实际开发中应尽量避免。
Spring Boot中处理循环依赖的方法
在Spring Boot中处理循环依赖主要有以下几种方法:
重新设计:
- 重新设计代码结构,消除循环依赖是最理想的解决方案。
使用@Lazy
注解:
- 通过延迟加载依赖对象来解决循环依赖问题。在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入。
使用Setter/Field注入:
- 在循环依赖的Bean中,使用Setter方法注入另一个Bean。
- Spring可以先创建Bean的实例,然后再通过Setter方法进行依赖注入。
- 对于Setter注入的循环依赖,Spring会从三级缓存或二级缓存中获取部分创建的Bean实例,提前暴露出来进行依赖注入,从而解决循环依赖问题。
构造器注入:
- 通过构造函数的方式将循环依赖的Bean注入到另一个Bean中。
- 但需要注意的是,构造器注入在循环依赖的情况下会抛出异常,因为构造器注入是一次性完成的,无法解决循环依赖的问题。
使用@Autowired和@Qualifier注解:
- 在循环依赖的Bean中,使用@Autowired注解注入另一个Bean,并使用@Qualifier注解指定要注入的Bean的名称。
- 这种方式可以解决由于多个相同类型的Bean导致的循环依赖问题。
使用@PostConstruct
注解:
- 在Bean的
@PostConstruct
方法中手动设置依赖关系。
实现ApplicationContextAware
与InitializingBean
接口:
- 通过这些接口获取BeanFactory或ApplicationContext,并在适当的时机设置依赖关系。
配置允许循环引用:
- 在Spring配置中设置
spring.main.allow-circular-references=true
来允许循环引用的存在。
注意事项
- 在处理循环依赖时,应尽量避免使用构造器注入,而是采用Setter注入或@Lazy注解等方式。
- 如果循环依赖的Bean中存在单例和原型模式的Bean同时存在的情况下,Spring会抛出异常。因为在创建Bean的时候无法确定它们的依赖关系。为了解决这个问题,可以将其中一个Bean的作用域改为原型模式,或者使用代理的方式解决循环依赖。
- 在实际开发中,最好设计出避免循环依赖关系的代码结构,从而保持代码的清晰和可维护性。