Java面试题:Spring中的循环依赖,给程序员带来的心理阴影

循环依赖通常发生在两个或多个Spring Bean之间,它们通过构造器、字段(使用@Autowired)或setter方法相互依赖,从而形成一个闭环。下面是一个使用字段注入(即使用@Autowired)导致的循环依赖的示例:

示例代码:

假设我们有两个类,ClassA 和 ClassB,它们相互依赖:

复制代码
public class ClassA {  

    @Autowired  
    private ClassB classB;  

    // ... 其他代码 ...  
}  

@Component  
public class ClassB {  

    @Autowired  
    private ClassA classA;  

    // ... 其他代码 ...  
}

在上面的示例中,ClassA 依赖 ClassB,而 ClassB 又依赖 ClassA。当Spring容器启动时,它会尝试为这两个类创建bean的实例。但是,由于它们之间的循环依赖,这会导致问题。

问题说明:

当Spring容器创建ClassA的bean时,它会发现ClassA依赖于ClassB,所以它会尝试创建ClassB的bean。当Spring容器创建ClassB的bean时,它又会发现ClassB依赖于ClassA,但此时ClassA的bean还没有被完全初始化(因为它正在等待ClassB的bean),这就形成了一个死循环。

那么Spring是如何解决这种循环依赖问题的?

三级缓存机制:

Spring容器在创建bean的过程中,会维护三个缓存,分别是:singletonObjects(一级缓存)、earlySingletonObjects(二级缓存)和singletonFactories(三级缓存)。

当Spring容器开始实例化一个bean时,会先将其ObjectFactory(对象工厂)放入三级缓存中。如果在实例化过程中需要注入其他bean,并且这个bean也正在实例化中,Spring会先从一级缓存中查找该bean的实例。如果没有找到,会到二级缓存中查找。如果二级缓存中也没有,那么会到三级缓存中查找ObjectFactory,并调用ObjectFactory的getObject方法来获取bean的实例。

当获取到bean的实例后,会将其放入二级缓存中,并从三级缓存中移除ObjectFactory。

当bean的实例化过程完成后,会将其放入一级缓存中。

通过这种方式,Spring可以在bean的实例化过程中解决循环依赖问题。

@Lazy注解:

在Spring中,可以使用@Lazy注解来延迟bean的初始化。当一个bean被标记为@Lazy时,Spring容器在启动时不会立即实例化它,而是在第一次被使用时才进行实例化。

通过将循环依赖的bean声明为懒加载,可以延迟它们的初始化过程,从而避免在容器启动时发生循环依赖问题。

需要注意的是,@Lazy注解只能用于单例作用域的bean,并且要求依赖项必须是接口类型。

以下是使用@Lazy注解来解决循环依赖的示例代码:

复制代码
@Component  
public class ClassA {  

    private final ClassB classB;  

    // 使用@Autowired和@Lazy注解来延迟ClassB的注入  
    @Autowired  
    public ClassA(@Lazy ClassB classB) {  
        this.classB = classB;  
    }  

    // ... 其他代码 ...  
}  

@Component  
public class ClassB {  

    private final ClassA classA;  

    // 使用@Autowired和@Lazy注解来延迟ClassA的注入  
    @Autowired  
    public ClassB(@Lazy ClassA classA) {  
        this.classA = classA;  
    }  

    // ... 其他代码 ...  
}

在上面的示例中,@Lazy注解被用于ClassA和ClassB的构造器参数上,以延迟它们之间的依赖注入。这意味着在创建ClassA时,它不会立即尝试去初始化ClassB,而是会得到一个代理对象。同样,在创建ClassB时,它也不会立即初始化ClassA。

避免构造器循环依赖:

在Spring中,构造器循环依赖是无法解决的,因为构造器在实例化bean的过程中被调用,如果两个bean相互依赖对方的构造器,那么就会形成死锁。

因此,在设计bean的依赖关系时,应该尽量避免使用构造器注入来创建循环依赖。可以使用setter注入或字段注入来代替构造器注入。

下面是一个构造器循环依赖的错误代码示例:

复制代码
@Component  
public class ClassA {  

    private final ClassB classB;  

    // 通过构造器注入ClassB,形成循环依赖  
    @Autowired  
    public ClassA(ClassB classB) {  
        this.classB = classB;  
    }  

    // ... 其他代码 ...  
}  

@Component  
public class ClassB {  

    private final ClassA classA;  

    // 通过构造器注入ClassA,与ClassA形成循环依赖  
    @Autowired  
    public ClassB(ClassA classA) {  
        this.classA = classA;  
    }  

    // ... 其他代码 ...  
}

在上面的示例中,ClassA和ClassB各自在构造函数中依赖于对方,这就形成了构造器循环依赖。

当Spring容器尝试创建这两个bean的实例时,会遇到问题:

  • Spring首先尝试创建ClassA的实例,并发现它需要ClassB的实例。

  • 然后,Spring尝试创建ClassB的实例,但发现它需要ClassA的实例。

  • 由于ClassA的实例正在等待ClassB的实例,而ClassB的实例又正在等待ClassA的实例,这导致了一个死循环,无法继续创建bean的实例。

综上所述,Spring通过三级缓存机制、@Lazy注解以及避免构造器循环依赖等方式来解决循环依赖问题。这些机制使得Spring容器能够更加灵活地处理bean之间的依赖关系,提高系统的可维护性和可扩展性。

相关推荐
张国荣家的弟弟2 分钟前
【Yonghong 企业日常问题 06】上传的文件不在白名单,修改allow.jar.digest属性添加允许上传的文件SH256值?
java·jar·bi
ZSYP-S13 分钟前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos20 分钟前
C++----------函数的调用机制
java·c++·算法
是小崔啊38 分钟前
开源轮子 - EasyExcel01(核心api)
java·开发语言·开源·excel·阿里巴巴
黄公子学安全1 小时前
Java的基础概念(一)
java·开发语言·python
liwulin05061 小时前
【JAVA】Tesseract-OCR截图屏幕指定区域识别0.4.2
java·开发语言·ocr
jackiendsc1 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
Yuan_o_1 小时前
Linux 基本使用和程序部署
java·linux·运维·服务器·数据库·后端
Oneforlove_twoforjob1 小时前
【Java基础面试题027】Java的StringBuilder是怎么实现的?
java·开发语言
数据小小爬虫2 小时前
利用Java爬虫获取苏宁易购商品详情
java·开发语言·爬虫