👉博主介绍: 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO TOP红人
Java知识图谱点击链接:体系化学习Java(Java面试专题)
💕💕 感兴趣的同学可以收藏关注下 ,不然下次找不到哟💕💕
✊✊ 感觉对你有帮助的朋友,可以给博主一个三连,非常感谢 🙏🙏🙏
文章目录
写在前面
🔔这个面试题是面到 Spring 必问的,要想很好的去回答还是要研究下源码的,而不是纯靠背八股文。接下我就剖析下如何解决的,面试也可以按照我介绍的去回答。结构化的表达会给面试官留下一个好印象,觉得你是一个逻辑清晰的开发。
1、什么是循环依赖
循环依赖指的是在软件开发中,两个或多个模块之间存在相互依赖的情况,形成了一个闭环。换句话说,一个模块依赖于另一个模块,而另一个模块又依赖于第一个模块,从而导致无法解决依赖关系。
而Spring循环依赖是指在使用Spring框架进行依赖注入时,两个或多个Bean之间存在相互依赖的情况,形成了一个循环的依赖关系。
当两个或多个Bean互相依赖时,Spring容器会尝试解析这些依赖关系并完成依赖注入。然而,如果存在循环依赖,Spring容器无法确定先实例化哪个Bean,因为它们相互依赖且互相等待对方被创建。
2、循环依赖会引起什么问题?
循环依赖可能引起以下问题:
-
🎄 无法完成依赖注入:循环依赖会导致依赖关系无法解析,因为每个Bean都依赖于另一个正在创建的Bean。这可能会导致Spring容器无法完成依赖注入,从而抛出异常。
-
🎄 死锁:循环依赖可能导致死锁情况。当两个或多个Bean相互等待对方被创建时,可能会出现死锁,阻塞应用程序的执行。
-
🎄 不稳定的行为:循环依赖可能导致不稳定的行为。由于循环依赖的存在,Bean的创建顺序变得不确定,可能会导致意外的结果或错误。
-
🎄 难以理解和维护:循环依赖会增加代码的复杂性,使代码难以理解和维护。当存在循环依赖时,代码的结构变得混乱,增加了调试和排查问题的难度。
3、看一个 Spring 循环依赖的例子
package com.pany.camp.circle;
import org.springframework.stereotype.Component;
/**
*
* @description: 测试类A
* @copyright: @Copyright (c) 2022
* @company: Aiocloud
* @author: pany
* @version: 1.0.0
* @createTime: 2023-07-10 13:01
*/
@Component
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
package com.pany.camp.circle;
import org.springframework.stereotype.Component;
/**
*
* @description: 测试类B
* @copyright: @Copyright (c) 2022
* @company: Aiocloud
* @author: pany
* @version: 1.0.0
* @createTime: 2023-07-10 13:01
*/
@Component
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
package com.pany.camp.circle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
*
* @description: 入口类
* @copyright: @Copyright (c) 2022
* @company: Aiocloud
* @author: pany
* @version: 1.0.0
* @createTime: 2023-07-10 13:01
*/
@SpringBootApplication
public class CircularDependencyExample implements CommandLineRunner {
@Autowired
private A a;
public static void main(String[] args) {
SpringApplication.run(CircularDependencyExample.class, args);
}
@Override
public void run(String... args) throws Exception {
System.out.println(a);
}
}
这是一个金典的循环依赖的代码,工作中可能会遇到,例如 service 层之间互相调用,就会引起循环依赖。Spring 也会将这个问题打印出了,我们看下面的日志:
4、如何解决循环依赖呢?
通过 @Lazy 注解进行懒加载
package com.pany.camp.circle;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
/**
*
* @description: 测试类A
* @copyright: @Copyright (c) 2022
* @company: Aiocloud
* @author: pany
* @version: 1.0.0
* @createTime: 2023-07-10 13:01
*/
@Component
public class A {
private B b;
public A( @Lazy B b) {
this.b = b;
}
}
5、Spring 是如何解决循环依赖?
Spring解决循环依赖的原理是通过使用三级缓存和提前暴露未完成的Bean来实现的。
当Spring容器创建Bean时,会经过三个阶段的缓存:单例对象缓存、早期对象缓存和已完成Bean缓存。
-
🎄 单例对象缓存:在创建Bean之前,Spring会先检查单例对象缓存中是否存在该Bean的实例。如果存在,直接返回该实例,避免重复创建。
-
🎄 早期对象缓存:如果单例对象缓存中不存在该Bean的实例,Spring会将正在创建的Bean实例放入早期对象缓存中。这时,Spring会创建一个ObjectFactory,用于提供一个代理对象,以便在创建过程中解决循环依赖。
-
🎄 已完成Bean缓存:当Bean的创建过程完成后,Spring会将其放入已完成Bean缓存中。这样,其他依赖该Bean的Bean就可以从缓存中获取到已完成的实例。
具体的解决循环依赖的过程如下:
-
🎄 创建A对象:当需要创建A对象时,Spring会首先创建一个A的早期对象,并将其放入早期对象缓存中。
-
🎄 注入B对象:在创建A对象时,发现A依赖于B对象。此时,Spring会检查B对象是否在早期对象缓存中。如果在缓存中找到B的早期对象,Spring会将该早期对象注入到A对象中。
-
🎄 创建B对象:接下来,Spring会创建B对象。在创建过程中,发现B依赖于A对象。由于A对象已经在早期对象缓存中,Spring会将A的早期对象注入到B对象中。
-
🎄 完成A对象:当A对象的创建过程完成后,Spring会将其放入已完成Bean缓存中。
-
🎄 完成B对象:最后,当B对象的创建过程完成后,Spring会将其放入已完成Bean缓存中。
通过使用三级缓存和提前暴露未完成的Bean,Spring能够在循环依赖的情况下正确地解决依赖注入的问题。这样,即使存在循环依赖,Spring也能够确保依赖的Bean在正确的时间点被注入,避免了循环依赖导致的无限递归或空指针异常等问题。
6、面试还会补充问“二级缓存能解决循环依赖问题吗?”
答案是肯定,只不过代码上就要调整了。解决循环依赖,跟具体使用几级缓存其实没多大关系,主要还是在创建对象这个过程提前暴露,这个才是关键。
💕💕 本文由激流原创,原创不易,希望大家关注、点赞、收藏,给博主一点鼓励,感谢!!!
🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃