Spring循环依赖的解决办法!包含代码讲解!!!
大家面试相信遇到过这么一个问题。
面试官问:你知道spring的循环依赖吗,可以讲一下吗?
我:这个我熟啊,循环依赖就是spring构造一个bean对象A,但是A里面有一个属性为B,所以spring回去创建我们的B,但是B对象创建的时候,填充属性的时候又发现的他有个属性为A,这就造成了我们的循环依赖。我们可以使用三级缓存来解决它。
面试官:你可以走了~~~
我:好嘞!
哈哈哈,一个小插曲,虽然说简洁的说确实是这样,但是你这样说肯定是不行的,像是在背八股文。
首先,你要回答这个问题你必须得了解spring的bean它的一个生命周期。还有spring的依赖注入的方式。
首先spring的依赖注入大概可以分为两点,一个是set注入,另外一个是构造器注入。spring可以解决掉我们的setter注入的方式,还有就是单例模式下的循环依赖,构造器注入和多例模式都不行。
spring的生名周期大概又可以分为四点,首先是createBeanInstance实例化,接着populateBean属性赋值,然后是initializeBean初始化,最后销毁。
而且还得提到一下spring的三级缓存
//一级缓存 主要是用于存放完全初始化好的bean对象
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
//二级缓存 主要是用于存放 还未填充属性的最原始的bean对象(用于解决循环依赖)
private final Map<String, Object> EarlySingletonObjects = new ConcurrentHashMap<>(16);
//三级缓存 主要是存放ObjecttFactory bean的工厂对象 (用于解决循环依赖)
private final Map<String, Object> singletonFactories = new ConcurrentHashMap<>(16);
我们的spring在启动的时候会去扫描看哪些需要注册bean 当扫面后会把它变成一个BeanDefintion然后存入一个BeanDefintion Map中,然后遍历这个Map,做一些验证,比如说是否是单例,是否懒加载等等,然后他会首先去获取这个bean是否已经存在于单例池中,有没有被提交前暴露,然后他会去判断哪个构造方法最优。接着我们的Spring就会通过反射去实例化一个JAVA对象,对这个bean做一些初始化的操作,比如说是否需要去合并BeanDefintion,然后他就会判断我们Spring容器是否支持循环依赖,支持的话,他就会暴露原始的bean对象到二级缓存中,接着会将bean的一个工厂类暴露到三级缓存中。
当A对象填充属性的时候,发现需要B就会依次将bean的工厂对象暴露到三级缓存中然后去实例化B,而B填充属性时,发现需要A,就会去单例池获取A,发现没有,就会去二级缓存获取,但是二级缓存中的A已经暴露到了三级缓存的工厂中,所以他会去三级缓存中取这个半成品A,然后B的bean创建完成,放入单例池中,A再去填充属性。
话不多说,上图:
上代码:
下述为循环依赖的代码示列:
//循环依赖
@Component
public class A {
@Autowired
private B b;
public void hello(){
System.out.println("helloA!!!");
}
}
@Component
public class B {
@Autowired
private A a;
public void hello(){
System.out.println("helloB!!!");
}
}
@SpringBootTest
class DemoApplicationTests {
@Autowired
A a;
@Test
void contextLoads() {
a.hello();
}
}
//报错
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| a
↑ ↓
| b (field private com.example.demo.test.A com.example.demo.test.B.a)
└─────┘
解决办法:
主要的解决方法有三种,一种是setter注入,第二种是@Lazy,第三种是@PostConstruct
1.@Lazy 在需要注入的属性前,表示延迟注入
@Component
public class A {
@Autowired
private @Lazy B b;
public B getB() {
return b;
}
public void hello(){
System.out.println("helloA!!!");
}
}
@Component
public class B {
@Autowired
private @Lazy A a;
public void hello(){
System.out.println("helloB!!!");
}
}
2.使用setter/filed注入,spring的官方推荐也是使用的该方法。
public class A {
private B b;
public B getB() {
return b;
}
@Autowired
public void setB(B b) {
this.b = b;
}
public void hello(){
System.out.println("helloA!!!");
}
}
@Component
public class B {
private A a;
public A getA() {
return a;
}
@Autowired
public void setA(A a) {
this.a = a;
}
public void hello(){
System.out.println("helloB!!!");
}
}
3.使用@PostConstruct,@PostConstruct注解的方法将会在A注入完成后被自动调用。
@Component
public class A {
@Autowired
private B b;
public B getB() {
return b;
}
@PostConstruct
public void init(){
b.setA(this);
}
public void hello(){
System.out.println("helloA!!!");
}
}
public class B {
private A a;
public A getA() {
return a;
}
public void setA(A a) {
this.a = a;
}
public void hello(){
System.out.println("helloB!!!");
}
}
终章
方法肯定不止这三种,小编在这里只是举例,还有很多种方法可以解决,但是当出现这种问题的时候,一般我们要去想办法,如何让我们的代码不去循环依赖。我觉得主要的是反思自己的代码设计,而不是去解决循环依赖。