开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
什么是循环依赖
@Component
class Bean1{
@Autowired
private Bean2 bean2;
}
@Component
class Bean2{
@Autowired
private Bean1 bean1;
}
如上述代码所述,Bean1注入了Bean2,Bean2注入了Bean1,你中有我,我中有你,就是循环依赖。那这样有什么不妥呢,我们都知道在Bean的生命周期中有一个步骤是需要注入属性的,也就是Spring关键的一个地方,依赖注入。如果创建Bean1的时候,对象已经构造完毕,执行到注入阶段,发现Bean2还没有被创建成功,那么就会先去创建Bean2,同理,Bean2和Bean1互相卡死,造成了循环依赖的局面。
在SpringBoot2.6版本以后,默认已经不支持循环依赖了。确实,循环依赖尽管后面会详细讲解一下各种解决的办法,但是这种非必要情况下,是非常不建议写出循环依赖的代码的。如果出现,那么就要检查检查整体代码架构设计问题了。
三级缓存
循环依赖之所以出现,就是因为Bean之间发生了循环,如果有一个介质来打破这层循环,那么循环依赖也就被解决了,这里引入一个关键点:三级缓存。了解解决循环依赖的原理之前,先来了解一下三级缓存都是什么。
一级缓存:singletonObjects
singletonObjects可以理解为单例池,当Bean的所有创建步骤都已经执行完毕的时候,已经是一个完全整体的Bean对象的时候,就会放入到该池中。当其他Bean需要进行依赖注入使用的时候便会从该池中寻找目标Bean。
二级缓存:earlySingletonObjects
这里名字上其实和singletonObjects很像,只是多了一个early,顾名思义,二级缓存也就是早期的单例Bean,是指还未完全执行完Bean创建周期的Bean。靠这一级缓存,就可以在发生循环依赖的时候,先给需要注入的Bean注入一个未完全创建好的Bean的引用,这样先解决循环问题。等到双方都互相注入后并变成了一个完整的Bean,这时再从二级缓存中删除,都拿到一级单例池中来。这样引用一个未完成的Bean就变成了引用一个完整的Bean。
三级缓存:singletonFactories
在解释三级缓存前,先解决一个面试题,那就是循环依赖用两级缓存可以解决么?通过上面两段的简单描述,我们可以看到,循环依赖被这两级缓存已经解决过了。二级缓存就是一个中间人,把发生循环的两个Bean或者更多个Bean之间先存起来供彼此使用。实际上,单从循环依赖的角度,确实用两级缓存即可,但是,Spring有一个重要的关键元素AOP,为了解决AOP便有了三级缓存。
由于AOP的存在,我们不知道依赖注入的时候是需要注入原始对象还是代理对象,这就需要添加一个逻辑来处理这个事情。三级缓存中和一二级中存放的东西有所区别,源码中三级缓存的map中存放的是一个lambda表达式,是用来处理该判断逻辑的。如果含有AOP相关配置或注解,就返回代理对象,否则返回原本对象。
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
在doCreateBean方法中有这么一段代码,这里的addSingletonFactory就是往三级缓存添加信息,getEarlyBeanReference方法一直跟踪会到wrapIfNecessary方法中。
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
三级缓存意义
上面所说的三级缓存处理代理对象的操作,按理来说是可以在实例化后就做的事情,这样的话,直接在Bean实例化后直接生成代理对象存入二级缓存,就不用三级缓存了。分析这个问题前,我们先回顾一下上一章节中提到的代理相关逻辑处理的时机,就是在非特殊Bean的情况下,AnnotationAwareAspectJAutoProxyCreator后处理器的postProcessAfterInitialization初始化后方法中才会进行代理。
换句话说,实例化后立刻就进行代理,比较违背Spring设计的理念,但是由于循环依赖的存在,不得不将代理的动作进行提前。不过虽然需要提前,但实例化后,还无法判断是否发生了循环依赖,也无法判断程序是否允许循环依赖,在这种情况下,不分情况直接生成代理对象就显得很不合适,毕竟循环依赖的情况在实际开发中也是少之又少,前面也提过了,如果发生循环依赖,最好是解决掉该设计问题。所以引入三级缓存将生成对象的lambda表达式逻辑封装到三级缓存中去,是一个比较好的解决办法。
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
doCreateBean调用了以上方法,看最中间的if语句,这里将三级缓存中表达式得到的对象放入二级缓存,同时删除了三级缓存中的表达式,三级缓存中的表达式只会调用一次,一旦调用,二级缓存中就已经有需要的对象,需要代理就是代理对象,不需要代理就是原始对象。
Bean的创建过程中,一定会被加入一级缓存和三级缓存,二级缓存只有发生循环依赖的时候才会出现,总体上来说,三级和二级缓存的目标就是为了将对象变成一级缓存中的单例对象。
earlyProxyReferences
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
这段代码上一章节提到过,这里再看一下,getEarlyBeanReference方法就是doCreateBean方法存入三级缓存中的lambda表达式最终调用的方法。这里往一个名叫earlyProxyReferences的集合里面存入了一个份提前创建的对象,然后在需要生成代理对象的步骤中,通过该集合进行判断。
构造注入下的循环依赖
这里我们对上面的简单代码做一个改动。
@Component
static class Bean1{
private Bean2 bean2;
//@Lazy
public Bean1(Bean2 bean2) {
this.bean2 = bean2;
}
}
@Component
static class Bean2{
private Bean1 bean1;
//@Lazy
public Bean2(Bean1 bean1) {
this.bean1 = bean1;
}
}
注解注入的方式改为了构造器注入,我们再次启动容器将会直接报错,我们回想两者的区别。最初的注解注入的例子中,我们是先对Bean进行实例化,然后存入三级缓存,然后填充属性进行初始化,初始化的时候发现出现了循环依赖,再然后做后续的操作。但是这里,由于我们自定义了有参构造,那么Bean在使用有参构造进行实例化的时候,就卡住了,实例化的时候,就出现了循环依赖。这和之前说的步骤有点不太一样,那么这种情况如何解决。
@Lazy注解解决循环依赖
相信很多人使用过这个注解来解决循环依赖,最初大家根据该注解的含义有可能会这么理解它的原理,就是加了该注解后,就会让程序知道这个Bean可以不用及时加载,只有真正使用到的时候才回去加载,两个出现循环依赖的Bean都不及时的注入自己属性,就不会出现循环依赖了。其实这种说法有点欠妥,因为这里其实是加载出来了一个对象来进行属性填充。
回看上面的程序,Bean1在加载到加了@Lazy注解的Bean1构造的时候,会对Bean2进行代理,生成一个Bean2的代理类,这里提到的代理和AOP没有任何关系,Spring会在很多地方使用cglib生成类的代理,这里提到的代理,就是对Bean2的一个代理,代理类会继承Bean2本身。这个代理类和AOP代理出来的有一个区别就是,这里的代理没有注入目标对象,在使用的时候,还是会从容器中去寻找目标对象然后进行方法调用。
有了一个代理类后,有参构造直接传入该代理类,那么整体Bean的创建就可以往下进行了。由于使用了代理方式解决,后面也不会出现循环依赖的问题。