在面试中常问的一个问题时bean的生命周期,其实在我看来bean的创建与销毁或许更合适。这篇文章中 Spring容器管理与Bean生命周期管理介绍了BeanDefinition的寻找与生成,下面我们探讨一下循环依赖与Spring提供的解决方式。
上图是一个bean创建的完整流程,图中将其提炼了一个关键节点,感兴趣的可以跟随流程一起debug看下流转的过程,回头再看这张图会更加清晰。
bean的创建入口在context.finishBeanFactoryInitialization ---->factory.preInstantiateSingletons()---->factory.doGetBean()
在bean创建的过程中会有一个循环依赖的问题,所谓循环依赖就是,一个完整的对象A、B、C,其属性可能是相互持有的,比如A有一个属性B,B有一个属性C,C有一个属性A。这里在创建的时候就会有一个顺序的问题。这里的循环依赖常常由@AutoWired注解以及构造函数注入产生的。对于属性依赖,我们可以先调用构造函数,后面通过一些set方法设置属性解决,但是对于构造函数的循环依赖Java语言目前是解决不了的。但是SpringBoot针对构造函数的循环依赖做了一点小处理,可以像下面这样用@Lazy注解来解决。原理就是通过创建一个代理的方式。先创建一个代理对象引用,后面再调用构造函数进行创建。所以本质上,注入的对象并不是我们代码预期中的属性,但是实际功能又没有什么差别,所以也算是解决了循环依赖。
@Component
public class Cat {
private Dog dog;
/**
* 构造方法注入属性对象时,被标注@Lazy注解的参数对象,并不会直接触发Dog类的实例化操作,
* 而是会先放入一个dog的代理对象在这里,当后面该dog的代理对象被第一次调用时,才会触发Dog的实例化操作,
* 但是生成的实例化对象并不会替换原本的代理对象,只会将实例化对象注入代理对象里面。
* (代理对象在其内部维护对实际实例的引用,在后续每次调用时,代理对象都会将请求转发给已经实例化的原生对象,并返回原生对象的方法执行结果)
* @param dog
*/
@Autowired
public Cat(@Lazy Dog dog) {
//注意这里不能在构造方法里面调用dog对象,一旦调用就会立即触发Dog的实例化操作
this.dog = dog;
}
}
@Lazy注解会导致对象的延迟创建,在真正调用对象的某个方法的时候才会调用构造函数。而@AutoWired属性依赖是在图中的第二步就解决的,下面我们看下Spring是如何解决这种依赖的。先说结论,就是大家常听到的三级缓存。
缓存字段名 | 缓存级别 | 数据类型 | 描述 |
---|---|---|---|
singletonObjects | 1 | Map<String, Object> | 存储 Bean 的完成品,完全初始化 |
earlySingletonObjects | 2 | Map<String, Object> | 存储 Bean 的半成品,尚未完成属性填充和初始化 |
singletonFactories | 3 | Map<String, ObjectFactory> | 存储创建 Bean 的 ObjectFactory 对象,生成半成品 Bean 放入二级缓存 |
三个缓存中只会同时存在一个值,放入一个Map的时候会加锁移除原先的Map中的值。
在分析Spring的三级缓存之前,我们先自己想一下我们自己设计的话应该怎么去解决循环依赖。
其实一个Map就可以解决了,我们调用了构造函数后生成a,就将其放入Map中,然后对其进行属性填充,发现依赖B,就去调用构造函数创建b丢进Map,然后将b设置给a,a的属性注入完成,Map中的a也成长成了A,再注入B的属性。这得益于构造函数调用完后的引用不会随着属性的注入而改变。
这里我们再考虑一下代理的问题,因为代理会改变原先构造函数调用完后的引用地址,所以需要额外考虑。比如,我们需要为B创建代理对象,那这个属性注入与对象创建的时机怎么决定,一个Map还能解决吗?
答案是一个Map也可以解决。我们在注入a的属性b的时候需要提前判断b是否需要创建代理,那万一a也需要创建代理呢?所以我们需要将创建代理的判断提到创建对象之前。不管创建什么对象,上来就判断是否需要创建代理。比如判断a需要创建代理,则先创建代理proxyA,然后再调用具体a的构造函数创建a将其作为一个参数给proxyA,再填充a的属性b,同样的也去判断b是否需要创建代理这些操作。然后ProxyA中的a成长成A,ProxyA也初始化完毕。这得益于代理对象中proxyA持有的是a的引用,先创建ProxyA后初始化a,和先创建a初始化再创建ProxyA产生的结果是一样的。
这里有两个问题,
- 一个Map中可能同时存在初始化完成的H,和未完全创建完成的a(属性未注入)。逻辑上有问题,要解决这个问题必须两个以上的Map进行解决。一级缓存Map存放初始化完成可以放心使用的对象,二级Map存放临时的半成品。调用了构造函数后生成a,就将其放入二级缓存Map中,最终属性注入完成后移除二级Map对象,放入到一级缓存Map中去。对于需要代理的场景处理方式也一样。
- 这里还有一个问题,我们正常创建代理是先创建完对象A再去为A创建代理,这里有点本末倒置了,而且不管需不需要,上来就判断一下。
为了解决这个问题,我们可以把创建代理的判断移到创建完对象A之后,如果发生循环依赖时,就去判断是否需要创建代理,没有发生循环依赖就创建完A后判断其是否需要创建代理,如果需要则创建。但是想象一下这样场景,A依赖B,B依赖C和A,但是C也依赖A。B中注入A的时候需要判断其是否需要创建代理,如果需要则要将二级缓存Map中的a移除为其创建一个代理对象放进去。同样的创建C时也需要判断A是否需要创建代理对象,同时还要判断Map中的A是否已经创建过了代理,创建过了还不能再次创建。本质就是要保证B和C中注入的A要一样,并且A创建完了之后,也需要判断自己是否已经创建过代理,创建过了还需要将自己作为一个参数设置给ProxyA。
这里可以看到两个Map虽然能解决掉这两个问题,但是条件是比较严格的,spring对于此进行了优化,提出了三级缓存来解决。其原理和二级缓存的情况下类似,这里就不再画流程图了。
一进来会放一个工厂方法进入三级缓存Map,如果没有发生循环依赖时,会正常的执行自己的一些初始化操作,并且在调用工厂中的BeanPostProcessor的后置处理方法时判断是否需要创建代理。代理的一些操作比如切片等都是BeanPostProcessor来处理,这个接口允许我们在创建对象放入到容器中最后一步之前做一些修改。
对于发生循环依赖的场景,如B也依赖A,会去三个Map中拿值,最先的是三级缓存,里面有一个A的工厂方法,会去调用getEarlyBeanReference()方法,这里会执行相应的BeanPostProcessor去判断是否需要创建代理,放入到二级缓存中去。创建C的时候直接去二级缓存中拿A就好了。最后A创建完了之后,执行BeanPostProcessor时,如果发现自己已经创建过了代理也不会再去创建。
也就是说,只有在发生了循环依赖的时候才使用到二级缓存,并且二级缓存也会去判断是否需要创建代理对象。