Spring循环依赖
什么是Spring循环依赖?
这个很好理解,就是多个bean之间相互依赖,形成了一个闭环。
比如:A依赖于B、B依赖于C、C依赖于A。
代码中表示:
public class A{
B b;
}
public class B{
C c;
}
public class C{
A a;
}
如何解决循环依赖?
Spring为了解决这个问题,引入了一个三级缓存。
这里我们假设一个场景进行讲解:ServiceA、ServiceB相互依赖。
- Spring容器依次创建两个bean时,发现在缓存中没有ServiceA,因此将新创建好的未注入属性的ServiceA放到三级缓存中去。
- 然后ServiceA进行属性注入时,发现依赖ServiceB,转而去实例化B。
- 同样创建对象ServiceB,注入属性时发现依赖ServiceA,依次从一级到三级缓存查询ServiceA,从三级缓存通过对象工厂拿到ServiceA,把ServiceA放入二级缓存,同时删除三级缓存中的ServiceA,此时,ServiceB已经实例化并且初始化完成,把ServiceB放入一级缓存。
- 接着继续创建ServiceA,顺利从一级缓存拿到实例化且初始化完成的ServiceB对象,ServiceA对象创建也完成,删除二级缓存中的ServiceA,同时把ServiceA放入一级缓存
- 最后,一级缓存中保存着实例化、初始化都完成的ServiceA、ServiceB对象
总的来说,当一个bean具有循环依赖时,是按照如下操作解决循环依赖问题的:
1.从容器中获取serviceA
2.容器尝试从3个缓存中找serviceA,找不到
3.准备创建serviceA
4.调用serviceA的构造器创建serviceA,得到serviceA实例,此时serviceA还未填充属性,未进行其他任何初始化的操作
5.将早期的serviceA暴露出去:即将其丢到第3级缓存singletonFactories中
6.serviceA准备填充属性,发现需要注入serviceB,然后向容器获取serviceB
7.容器尝试从3个缓存中找serviceB,找不到
8.准备创建serviceB
9.调用serviceB的构造器创建serviceB,得到serviceB实例,此时serviceB还未填充属性,未进行其他任何初始化的操作
10.将早期的serviceB暴露出去:即将其丢到第3级缓存singletonFactories中
11.serviceB准备填充属性,发现需要注入serviceA,然后向容器获取serviceA
12.容器尝试从3个缓存中找serviceA,发现此时serviceA位于第3级缓存中,经过处理之后,serviceA会从第3级缓存中移除,然后会存到第2级缓存中,然后将其返回给serviceB,此时serviceA通过serviceB中的setServiceA方法被注入到serviceB中
13.serviceB继续执行后续的一些操作,最后完成创建工作,然后会调用addSingleton方法,将自己丢到第1级缓存中,并将自己从第2和第3级缓存中移除
14.serviceB将自己返回给serviceA
15.serviceA通过setServiceB方法将serviceB注入进去
16.此时serviceA实例化可以完成(创建完成,然后将自己丢到第1级缓存并将自己从第2级缓存中剔除)
为什么要使用三级缓存?二级缓存不可以吗?
不可以,主要是为了生成代理对象。如果使用二级缓存,会导致被暴露出去的和最终的bean不是同一个bean。
可能有点难理解,这里需要一个例子进行讲解:该例子给Spring的getSingleton(beanName, false)
传入了一个false参数,表示不会从三级缓存中获取bean,等同于现在只有两级缓存。
Service1
@Component
public class Service1 {
public void m1() {
System.out.println("Service1 m1");
}
private Service2 service2;
@Autowired
public void setService2(Service2 service2) {
this.service2 = service2;
}
}
Service2
@Component
public class Service2 {
public void m1() {
System.out.println("Service2 m1");
this.service1.m1();// 这里调用了Service1的m1方法
}
private Service1 service1;
@Autowired
public void setService1(Service1 service1) {
this.service1 = service1;
}
public Service1 getService1() {
return service1;
}
}
然后我们需要在Service1上面加个拦截器,要求在调用Service1的任何方法前都要输出一行日志你好,service1"
@Component
public class MethodBeforeInterceptor implements BeanPostProcessor {
@Nullable
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if ("service1".equals(beanName)) {
//代理创建工厂,需传入被代理的目标对象
ProxyFactory proxyFactory = new ProxyFactory(bean);
//添加一个方法前置通知,会在方法执行之前调用通知中的before方法
proxyFactory.addAdvice(new MethodBeforeAdvice() {
@Override
public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
System.out.println("你好,service1");
}
});
//返回代理对象
return proxyFactory.getProxy();
}
return bean;
}
}
按照我们的思路,应该说无论是Service1还是Service2调用m1方法,最终的结果都应该是有日志。但是事与愿违,
在只有两级缓存的情况下,并不会出现我们预期的情况:
System.out.println("----A-----");
service2.m1();
System.out.println("----B-----");
service1.m1();
System.out.println("----C-----");
----A-----
Service2 m1
Service1 m1
----B-----
你好,service1
Service1 m1
----C-----
是的,在这里你可能会猜测,是不是service2中注入的service1不是代理对象,所以没有加上拦截器的功能?恭喜你,你的猜想是正确的,上面代码注入到service2中的service1是早期的service1,而最终spring容器中的service1变成一个代理对象了,早期的和最终的不一致了。
注意:结合上面的循环依赖解决流程,我们可以判断出Service2此时在二级缓存中找到Service1**(注意此时还没有进行代理)**,然后将Service1提升到一级缓存,注入Service1,完成一系列操作后Service2返回给Service1,然后Service1的实例化才完成,**才会开始进行代理。**不同于三级缓存,两级缓存的情况下这里会更新一级缓存。
因为service2中注入的是早期的service1,注入的时候service1还不是一个代理对象,所以没有拦截器中的功能。
那么,我们怎么在三级缓存的情况下解决上面Service2调用m1方法没有日志的情况呢?
自定义一个SmartInstantiationAwareBeanPostProcessor
,然后在其getEarlyBeanReference
中来创建代理:
@Component
public class MethodBeforeInterceptor implements SmartInstantiationAwareBeanPostProcessor {
@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
if ("service1".equals(beanName)) {
//代理创建工厂,需传入被代理的目标对象
ProxyFactory proxyFactory = new ProxyFactory(bean);
//添加一个方法前置通知,会在方法执行之前调用通知中的before方法
proxyFactory.addAdvice(new MethodBeforeAdvice() {
@Override
public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
System.out.println("你好,service1");
}
});
//返回代理对象
return proxyFactory.getProxy();
}
return bean;
}
}
因此,我们可以总结下这个问题:三级缓存可以验证早期暴露的bean被其他bean使用过,并且用来判断被使用的和最终暴露的不一致的问题,二级缓存做不到。二级缓存无法判断早期bean是否被使用过,早期bean若没被使用过就无需判断早期暴露的bean和最终bean是否一致的问题。
循环依赖无法解决的情况
只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
那就会有下面几种情况需要注意。
还是以2个bean相互依赖为例:serviceA和serviceB
情况1
条件
serviceA:多例
serviceB:多例
结果
此时不管是任何方式都是无法解决循环依赖的问题,最终都会报错,因为每次去获取依赖的bean都会重新创建。
情况2
条件
serviceA:单例
serviceB:多例
结果
若使用构造器的方式相互注入,是无法完成注入操作的,会报错。
若采用set方式注入,所有bean都还未创建的情况下,若去容器中获取serviceB,会报错,为什么?我们来看一下过程:
1.从容器中获取serviceB
2.serviceB由于是多例的,所以缓存中肯定是没有的
3.检查serviceB是在正在创建的bean名称列表中,没有
4.准备创建serviceB
5.将serviceB放入正在创建的bean名称列表中
6.实例化serviceB(由于serviceB是多例的,所以不会提前暴露,必须是单例的才会暴露)
7.准备填充serviceB属性,发现需要注入serviceA
8.从容器中查找serviceA
9.尝试从3级缓存中找serviceA,找不到
10.准备创建serviceA
11.将serviceA放入正在创建的bean名称列表中
12.实例化serviceA
13.由于serviceA是单例的,将早期serviceA暴露出去,丢到第3级缓存中
14.准备填充serviceA的属性,发现需要注入serviceB
15.从容器中获取serviceB
16.先从缓存中找serviceB,找不到
17.检查serviceB是在正在创建的bean名称列表中,发现已经存在了,抛出循环依赖的异常