前言
这段时间维护公司的一个springboot项目,开发功能的过程中,由于两个Service互相注入了对方的实例,导致项目启动时就报了循环依赖
异常。循环依赖这个东西,平时只出现在了面试题里,这次被自己碰上了,就仔细去研究了一下解决方式,顺带着复习了循环依赖问题。这里就做一个分享,希望可以帮到大家,谢谢!
场景重现
这里就简单写两个Service,让它们互相注入实例,只是为了复现循环依赖的问题,业务逻辑就不写了。会出现两个Service互相调用的情况,也有可能是因为前期代码架构设计不合理
,在这里就不讨论了。
AService
java
public interface AService {
}
java
@Service
public class AServiceImpl implements AService {
@Autowired
private BService bService;
}
BService
java
public interface BService {
}
java
@Service
public class BServiceImpl implements BService {
@Autowired
private AService aService;
}
项目中存在上述两个Service,在项目启动时,会抛出异常:
概念介绍
在介绍解决方案之前,我们先来回顾一下Spring的循环依赖问题。
在Spring框架中,bean的创建遵循着实例化->初始化
的过程:
-
框架先通过不同类型的BeanDefinitionReader,去读取不同类型的bean定义信息,创建出BeanDefinition。
-
然后通过反射的方式,去实例化bean,此时的bean是一个不完整的bean,只有一个引用,内部的属性还未填充。
-
然后进入bean的初始化流程,通过populateBean方法,为bean去填充属性。完成这一步才完成了bean的创建。
Spring的循环依赖,简单来说就是两个及以上的bean之间,互相注入了对方的实例,导致在创建bean的时候,出现的死循环的情况。例如A B两个bean,框架在实例化A时,发现需要注入B,然后先去实例化B,然后在实例化B的过程中,又发现需要去注入A,然后再去实例化A,以此类推。
画个图理解一下
源码解读
下面在SpringBoot v2.7.6的环境下(对应Spring版本v5.3),跟踪一下这块的源码,对整个流程加深一下理解。
提供一张流程图,可以参考着看一下,帮助理解
refresh方法
这里从主启动类中的run
方法开始:
调用了SpringApplication
类中的重载run方法
最后调用了下面这个重载方法,在这个方法中,refreshContext
这个方法的调用,作用是刷新上下文,Spring中的refresh
方法就是从这里调用。
如下,在这个方法中,调用了一个本类中的refresh
方法
这个方法中,进行了applicationContext
的refresh
方法调用
这个进入的是AbstractApplicationContext
这个类,refresh方法就是在这个类中定义的。
在这个方法中,进行了finishBeanFactoryInitialization
方法调用,该方法的作用是,完成剩余的单例bean的创建。
finishBeanFactoryInitialization
方法中,通过调用beanFactory
工厂对象的preInstantiateSingletons
方法,完成剩余单例bean的创建。
preInstantiateSingletons
这个方法中,就进行了所有还未实例化的单例bean的实例化,我们来看一下,具体的流程。
容器中不只有我们定义的两个Service,还有其他很多容器自带的bean,在这里我们加一个断点,同时进行一些设置,方便我们直接来观察AService的创建过程:
方法一开始,获取到了beanNames
,这是一个List集合,里面封装了容器需要实例化的所有的bean的名称。
在处理过程中,先去判断了这个bean,是否 不是抽象&不是懒加载&是单例bean
,是的话才进行该bean的实例化。
然后又判断了当前bean是否为FactoryBean
,如果不是的话,直接调用getBean
方法去获取实例,否则需要进行FactoryBean相关的处理。
FactoryBean:该类bean不受bean生命周期的控制,整个对象的创建过程是由用户自己来处理的,更加灵活。只需要调用getObject就可以返回具体的对象。
我们的AService是正常的需要进行生命周期管理的bean,所以直接进入getBean
方法,该方法中进一步调用了doGetBean
方法。
在Spring的源码中,凡是
do
开头的方法,一般都是真正干活的方法。例如doGetBean、doCreateBean等。
getSingleton方法
doGetBean方法中,第一步调用了getSingleton方法,目的是从一级缓存中尝试查找当前bean。
可以看到,调用了DefaultSingletonBeanRegistry
类中的getSingleton
方法之后,又调用了一个重载方法,第二个参数为allowEarlyReference
,是一个布尔值。这个参数的含义其实是是否允许提前暴露对象引用
。
提前暴露对象引用:这是一种试图解开循环依赖闭环的方法。当A实例化之后,将这个不完整的对象的引用,放到一个Map集合中,然后去注入它的属性b;此时去实例化B,发现需要属性a,这时,就可以从Map集合中,获取到不完整的A对象,因为此时已经有了对象的引用了,完全可以在后续的流程中,根据引用来找到对象,再对其进行赋值。这样就避免了在注入属性的过程中,一直闭环了。
看一下重载的getSingleton方法,这里面用到了三个Map集合,这里其实就是所谓的三级缓存
,是Spring用来解决循环依赖问题的关键。
上面的代码我们可以看出,当前方法的职责就是,从一级缓存(完整bean实例)以及二级缓存(不完整bean实例)中,查找当前bean。
- 如果查找不到,且当前bean是在创建中的状态,说明他可能已经创建了不完整对象,这时再去二级缓存查找。
- 如果二级缓存中还是没有,判断是否允许提前暴露对象引用,如果是,从三级缓存中获取当前bean的代理工厂对象,创建bean实例,放到二级缓存,同时清除三级缓存。
- 最后返回从一级缓存或者二级缓存中读取的bean实例。
在容器获取bean实例的时候,会从一级缓存以及二级缓存中读取,如果当前bean 已经放到了二级缓存,这里就可以拿到,而不是进入之前提到的闭环,这里就跳出了循环依赖。
再回到doGetBean方法中:
我们根据上述过程进行AService的获取,在这里获取到的肯定是一个null。
继续往下走,会再次调用另一个重载的getSingleton方法,这里还是尝试去获取bean实例。
不同的是,这里第二个参数 传入了一个lambda表达式,里面封装的是:如果还是获取不到bean实例,要进行的操作。可以看到这里调用了createBean方法:也就是说,如果这里还是获取不到bean实例,就要去创建bean实例了。
如下图,进入方法后,会再次从一级缓存中,尝试查找当前bean,如果还是没有,会调用传入lambda表达式的getObject方法,实际就是去调用了createBean方法,去创建bean实例。
createBean方法
createBean方法中,会进行doCreateBean方法的调用,之前说过凡是do开头的方法,一般都是真正干活的方法
,所以这里就是要进行bean的创建了。
doCreateBean方法中,是通过一个instanceWrapper
对象,来进行了bean的实例化,可以看到,首先通过调用createBeanInstance
方法,去创建了bean实例,并且将创建后的实例,封装到了wrappedObject
属性中,然后再调用getWrappedInstance
方法,获取到了实例,这里是不完整的对象,还未填充属性
。
看一下createBeanInstance
方法的具体实现:
最后一行调用了关键方法:instantiateBean
获取到实例化策略,调用了instantiate
方法
调用了BeanUtils
的instantiateClass
方法。
最后通过调用构造器
对象的newInstance
方法创建了实例
再回到doCreateBean方法中:
截至这里,bean的实例化过程结束了,现在获取到了一个不完整的bean实例,只创建了引用,还未填充属性,下面的逻辑就是通过populateBean
方法填充属性了。
populateBean方法
如下图,在doCreateBean
方法中,实例化对象之后,调用了populateBean
方法
populateBean方法中,有一段关于BeanPostProcessor的处理逻辑,是去获取BeanPostProcessorCache
对象,拿到里面的List集合instantiationAware
,去遍历它。
这里的遍历出来的元素,个人理解应该是可以去应用到当前bean上面的后处理器,遍历出来挨个对当前bean进行处理。
其中有一个后处理器叫做AutowiredAnnotationBeanPostProcessor
,个人理解是去扫描bean中@Autowired
标注的属性,并把他们注册到bean中。这里看一下这个后处理器中对应的postProcessProperties
方法:
该方法的第一行,调用findAutowiringMetadata
方法,获取到了当前bean通过Autowired需要去注入的属性,可以看到是bService
接下来调用metadata
对象的inject
方法:方法中遍历elementsToIterate
集合,集合中只有一个元素,就是AutowiredFieldElement
、字面意思也就是Autowired字段元素,个人理解也就是被Autowired注解标注的属性。
这里再次调用了AutowiredFieldElement
的inject
方法:
接着调用了resolveFieldValue
方法,字面意思是去解析字段的值。
接着调用了beanFactory
的resolveDependency
方法
在这个方法中,调用了doResolveDependency
,又一次看到do开头的方法,这就是真正干活的方法,跟进去
这个方法中,存在一行关键代码:descriptor.resolveCandidate
点进这个方法,可以看到,这里又调用了beanFactory
对象的getBean
方法,这次传入的beanName是BServiceImpl
,也就是说要去获取BService,然后填充给AService中的属性bService。
接下来就跟上面的流程一样,去经历getBean、doGetBean、getSingleton、createBean、populateBean 方法等,最后依然会走到descriptor.resolveCandidate
方法,此时的autowiredBeanName
的值变成了AServiceImpl
,也就是需要注入的bean的名称是AServiceImpl
,要将它注入到BService中
然后又会走到beanFactory.getBean
方法,传入的beanName这次是AServiceImpl
,这里为了填充BService的属性,又要去获取AServiceImpl
再次走到doGetBean
方法中的第二个getSingleton
方法中时,会遇到一个重要的方法调用:beforeSingletonCreation
方法,这里是在继续调用createBean
方法去创建bean之前的一个判断。
如下图,传入的beanName为AServiceImpl
,说明这里又是要去创建AService
beforeSingletonCreation
方法中,会进行两个集合的元素判断,后一个是singletonsCurrentlyInCreation
,看注释意思为当前正在创建过程中的bean名称
,执行到这里,如果发现这个集合中有当前传进来的bean名称,那意思岂不是就是我正在创建bean,突然发现这个bean已经在创建中了
,然后这里会抛出异常BeanCurrentlyInCreationException
走到这个异常类中可以发现,这里是发现了一个循环引用
:
最后会包装成为一个UnsatisfiedDependencyException
异常来抛出,最后变成InvocationTargetException
直接导致程序终止。
以上就是SpringBoot中,出现循环依赖时的流程分析了。
解决方案
优化代码结构
重新设计代码结构或调用流程,尽量避免两个Service互相调用的过程。
@Lazy
在其中一个Service使用@Autowired注解注入另一个Service时,添加@Lazy
注解,延迟它的加载。
java
@Service
public class AServiceImpl implements AService {
@Autowired
@Lazy
private BService bService;
}
java
@Service
public class BServiceImpl implements BService {
@Autowired
private AService aService;
}
原理解析
使用了上面的@Lazy注解,可以发现 的确没有出现循环依赖的问题。那为什么加了一个注解 就可以避免呢?下面我们来深究一下原理:
当@Autowired
注解标注在某个属性上时,在populateBean
方法执行到AutowiredAnnotationBeanPostProcessor
这个Bean后处理器时,会调用其内部类AutowiredFieldElement
的resolveFieldValue
方法,然后会调用到beanFactory
对象的resolveDependency
方法(上面的源码解读部分已经讲过了,不熟悉的朋友可以翻上去看一下)。
如果还同时标注了@Lazy
注解,那么在这个方法中,有一个关键调用:
getLazyResolutionProxyIfNecessary
这个方法,从字面意思来看,就是:如果必要,获取延迟代理对象
。这个地方 实际上是获取了一个代理对象,来替换了本该获取的注入属性对象。
点进这个方法看一下:
可以看到,先调用了isLazy
方法,来判断当前属性,是否标注了@Lazy
注解,如果是的话,调用了buildLazyResolutionProxy
方法,来创建了延迟代理对象。
再看一下这个方法:
后面的逻辑,就是创建了代理对象,当通过注入属性调用目标方法时,实际会调用到JdkDynamicAopProxy
类的invoke
方法中,实现目标方法的调用。(jdk相关原理,在其它文章有介绍,点击查看)。
综上所述 :标注了@Lazy注解之后,在注入属性时,不会再去容器中查找,而是创建一个代理对象
,替代需要注入的属性。同时,当前bean也作为一个完整对象,放到容器中。后续目标方法的调用,则是通过代理对象完成。
后记
以上,就是关于Springboot中出现循环依赖问题的解析了。感谢你的阅读,以上内容来源于自己的自学,如果有不对的地方,欢迎在评论区指正!谢谢~