闲谈
大家好,我是了不起,前段时间,了不起在当面试官,挑了许多人给leader去面谈,最后可能是因为把之前某个想走的同事留了下来了,所以对新人没有太多的要求,所以选了应届生。
感觉如果是这种情况,还是比较利好应届生的,不然有些业务比较特殊的活,需要有能力接下上一任的工作,对面试的人要求会非常的高,人也不好找,最后头疼的也是我。
不提也罢,回归正题,分享一道最近常用来面试1-2年工作经验的人的面试题吧。
什么是Spring的循环依赖问题
图片
在软件开发的世界里,我们总是追求代码的优雅与高效。目前Java主流的SpringBoot、SpringCloud框架无疑是我们最好的帮手。它不仅简化了企业级应用的开发,还为我们提供了许多强大的功能。
比如依赖注入DI,但是,就像任何技术都有其双刃剑的一面,依赖注入也不例外,Spring在进行依赖注入时最常见的一个问题——循环依赖。
举例一个场景,我们有两个Service类A和B,A类里有个a2方法需要调用了B类里的b1方法,B类里的b2方法需要用到A类里的a1方法。
图片
那么按照我们Java代码的无脑编程,就会是下面的这个情况:
ServiceA的a2方法调用ServiceB里的b1方法。
@Service
public class ServiceA implements Service {
@Autowired
private ServiceB serviceB;
@Override
public void a1() {
System.out.println("当前是ServiceA的a1方法"");
}
@Override
public void a2() {
System.out.println("这里将调用ServiceB的b1方法");
serviceB.b1();
}
}
同样ServiceB的b2方法就调用ServiceA里的a1方法。
@Service
public class ServiceB implements Service{
@Autowired
private ServiceA serviceA;
@Override
public void b1() {
System.out.println("当前是ServiceB的b1方法");
}
@Override
public void b2() {
System.out.println("这里将调用ServiceA的a1方法");
serviceA.a1();
}
}
运行结果
当你运行这个SpringBoot应用的时候,会遇到一个错误,错误信息如下:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Requested bean is currently in creation: Is there an unresolvable circular reference?
拓展复习
还记得Bean的创建过程吗?Spring 在启动时会根据配置文件或注解来创建和初始化所有的bean。这个过程可以分为几个阶段:
- 实例化(Instantiation):Spring 容器创建一个bean的实例。
- 属性填充(Population of Properties):Spring 容器设置bean的所有属性,包括依赖注入。
- 初始化(Initialization):Spring 容器调用bean的初始化方法(如 @PostConstruct 注解的方法或 InitializingBean 接口的 afterPropertiesSet 方法)。
现在我们有两个bean:ServiceA 和 ServiceB,它们相互依赖对方。具体来说:
- ServiceA 依赖 ServiceB
- ServiceB 依赖 ServiceA
当Spring尝试创建 ServiceA 时,它会发现 ServiceA 需要 ServiceB。于是Spring开始创建 ServiceB。然而,在创建 ServiceB 的过程中,Spring 又发现 ServiceB 需要 ServiceA。这时,Spring 发现自己已经在一个创建 ServiceA 的过程中,从而导致了一个循环依赖。
图片
好比这张图一样,把箭头的方向可以理解成前提条件,是不是就一目了然了。彼此成为对方的前提条件。就好比,不考虑进化论,究竟是先鸡还是先蛋?
回归正题
在开发中,一般遇到这个问题,通常会使用@Lazy来解决。
@Service
public class ServiceB implements Service{
@Autowired
@Lazy
private ServiceA serviceA;
@Override
public void b1() {
System.out.println("当前是ServiceB的b1方法");
}
@Override
public void b2() {
System.out.println("这里将调用ServiceA的a1方法");
serviceA.a1();
}
}
它一方面可以减少Spring的IOC容器在启动时的加载时间,一方面也可以解决Bean的循环依赖问题。
但是这是在日常开发使用的时候的处理方法,面试的时候肯定不会就这么放过你。
所以我们在面试的时候遇到这个问题,通常还会再多回答两个方式。
Spring解决循环依赖必须是单例的Bean
这是一种依赖Spring提前暴露对象的方式来实现的。这种也叫半成品对象,通过对上面的学习,我们知道了循环依赖的原因是因为在创建的时候需要引用到另一个正在创建的对象,通过暴露这种半成品对象,让初始化的时候能够解决循环依赖的问题。
但是这种方式不能使用在原型对象的创建和初始化!背过面试题的都知道:
- 单例对象的特点:
单例对象在整个容器生命周期内只会被创建一次。
这种特性使得单例对象的依赖关系在容器启动时就已经确定下来,不会发生变化。
- 原型对象的特点:
原型对象在每次请求时都会创建新的实例。
对于原型对象而言,每次创建新实例时都可能涉及到不同的对象实例,因此不能像单例那样缓存并复用半成品对象。
不支持构造函数注入
Spring无法解决构造函数导致的循环依赖,是因为在对象实例化的过程中,构造函数都是最早被调用的,那个时候对象还没完成实例化,所以没办法注入一个尚未完成创建的对象。
因此,解决循环依赖的一种方式,就是避开构造函数注入。
结论
上面的知识只是给你科普用的,不是让你用来回答的。如果你实在不理解,那就背下面的吧!
- 重新设计,彻底消除循环依赖(是一句废话没错,但是面试得讲一下)
- 改成非构造器注入的形式,比如setter注入或者字段注入
- 使用@Lazy解决