Spring当中的循环依赖问题是面试中经常提及的,这一次我们就来聊一聊这个问题。
首先,循环依赖其实指的就是多个类之间互相依赖(引用),当然在极限情况下也可以自己依赖自己。
首先先下结论,Spring解决循环依赖用的是三级缓存。
//一级缓存,这个就是我们大名鼎鼎的单例缓存池,用来保存我们所有的单实例bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
//二级缓存用,用户缓存我们的key为beanName,value是我们的早期对象(对象属性还没有来得记进行赋值),会返回代理对象的引用
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
//三级缓存是存放的是空壳,也就是我们的早期对象,该map用户缓存key为beanName value为ObjetFactory
private final Map<String, Object> earlySingletonObjects = new HashMap(16);
何谓三级缓存?在我看来,就是三个用来存放不同时期对象引用的Map。
接下来,我们就通过源码,来看看三级缓存到底是怎么运作的。
@Test
public void test2() {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
InstanceA instanceA = (InstanceA)context.getBean("instanceA");
InstanceB instanceB = (InstanceB)context.getBean("instanceB");
}
这是我们的测试类,我们在第二行打上断点,进行debug。
前面都是一些简单的方法传递,就不再赘述了。直到到了这里,这里得到的对象是null,没关系,记住这里,等一下你会豁然开朗的!
image.png
注:这里先额外提一句啊,我们看源码是不可能每一行都看的。所以要带有目的性的去看。所以我们可以根据方法的返回值来判断这一行代码有没有看的必要。
image.png
这里我们发现,虽然这行代码的返回值是我们想要的,但是它返回的是null。没有关系,先跳过这里。
image.png
很明显,这一行代码也要看,点进去,接下来就又到了这里。
image.png
然后就回到了lambda表达式这里,继续往里走。
image.png
注:这里再说一个小感触吧,Spring当中的方法,往往是带有do作为前缀的才是核心,就如这里的createBean方法和doCreateBean方法。
image.png
这个就是创建实例的核心方法,继续往下走。
image.png
看到没有,此时的instanceWrapper仍然为null,要调用createBeanInstance方法。看着这个方法的名字就知道,不远了!那这个方法它做了什么呢?其实,实例化就在这一步完成了。只不过,此时创建出来的对象是一个earlyObject——早期对象。什么意思呢?就是说,此时的对象它是一个空壳对象,并没有进行属性赋值。
接下来就到了这一步,我们依旧是点进去看看。
image.png
来了来了,这个方法如果熟悉源码的同学应该是经常看见的。这个方法也是解决循环依赖的核心所在。这个方法把早期对象包装成了一个ObjectFactory暴露到了三级缓存中。这里注意啊,所有的对象创建的过程中都会去把自己的早期对象(也就是上一步创建的空壳对象)暴露到三级缓存中去。
好,做完了这一步就到了这里了。
image.png
这个方法是在做什么?进行属性赋值!同学们还记得吗?我们的InstanceA可是依赖于InstanceB的哦。所以Spring就要给当前的这个InstanceA注入InstanceB。这个方法调用栈很长,在这里我就说一下核心的地方吧。
image.png
Spring会去解析InstancA中依赖了哪个字段,就比如说这里我们的InstanceA依赖于InstanceB,那他就会去调用getBean("InstanceB"),也就是说会去实例化InstanceA依赖的字段。那么InstanceB又会走刚刚InstanceA走过的流程,这一部分是一模一样的。那再InstanceB进行属性赋值的时候,它也会发现自己是依赖于InstanceA的。但这里就和刚才不同了哦。InstanceA已经被放到了三级缓存中,也就是最开始的那一步。
image.png
那么InstanceA就会在三级缓存中被找到,被找到之后会把对象从三级缓存移到二级缓存中去,请注意,这里是一个剪切操作而不是复制操作。
还记得这里吗?现在这里的InstanceA就可以获取到了!
image.png
这样InstanceB的依赖问题就已经解决了,就能够顺利的完成属性赋值。接下来完成对于bean的后续的处理之后,就可以返回InstanceB的实例化对象。返回给谁?当然是返回给InstanceA啦,InstanceA因为依赖于InstanceB还没完成属性赋值呢。这样,InstanceA也就能完成实例化了。在创建完毕之后,就会删除二级缓存,把单例对象存储到一级缓存中,也就是大名鼎鼎的单例缓存池。
至此,循环依赖的问题就已经被解决了!
那其实还有问题啊,那Spring为什么要使用三级缓存呢,为什么不用二级缓存呢?
原因就在于从三级缓存中获取到了早期对象时调用singletonFactory.getObject()会经过这个方法getEarlyBeanReference
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
//判断是否实现了SmartInstantiationAwareBeanPostProcessor接口
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
//执行实现的getEarlyBeanReference方法进行拓展刚实例化好的bean
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
if (exposedObject == null) {
return null;
}
}
}
}
return exposedObject;
}
这个方法有什么用呢?这个方法会经过Spring的BeanPostProcess,也就是后置处理器。所以它就会经过这个方法
image.png
会去调用wrapIfNecessary(bean, beanName, cacheKey)方法。这个方法它可以保证如果我当前这两个对象是代理对象的话,我们能拿到代理对象的引用。也就是说,它解决了代理对象的循环依赖。是不是还是有一定云里雾里的?没关系,看下面这两段代码!
@Service
public class UserServiceImpl {
@Autowired
private OrderServiceImpl orderService;
@Transactional
public void saveUser(){}
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private UserServiceImpl userService;
@Transactional
public void saveOrder(){}
}
这样类似的代码我相信同学们肯定都写过吧,两个service的循环依赖。这两个service都开启了事务,那么我们要拿到的就是这两个service的代理对象。而二级缓存就是帮我们拿到代理对象的引用。如果没有二级缓存,那我们的service拿到的就是原生对象的引用了。
好,对于Spring使用三级缓存解决循环依赖的整个流程我想大家都已经很明白了,如果还不是很明白,可以看一下下面这张图(是不是很贴心,hhhhhhhhhhhhhh)。
我们最后再来总结一下要点,所有的对象创建的过程中都会去把自己的早期对象暴露到三级缓存中去,三级缓存中所存放的都是空壳对象。当这个对象被其他依赖的对象在三级缓存中被找到时,会把对象从三级缓存移到二级缓存中去。经过二级缓存的对象如果是代理对象,那么就会返回它代理对象的引用。当对象创建完毕之后,就会删除二级缓存,把单例对象存储到一级缓存中,也就是大名鼎鼎的单例缓冲池中。
以上就是本人对于Spring解决循环依赖的一些浅见,本人才疏学浅,如有不对,还望批评指正。
网友评论