您的位置  > 互联网

Java面试题:循环依赖问题的解决办法

目前收藏量是点赞数的4倍多。 网上求赞,给作者一些鼓励。 一起努力。

最近,项目组的一位同事遇到了一个问题,向我征求意见,这立即引起了我的兴趣,因为我是第一次遇到这个问题。 我通常认为我对循环依赖问题有比较好的理解,直到我遇到了这个以及下面的问题,刷新了我的理解。

我们先看一下导致问题的代码片段:

@

班级 {

@

;

@异步

无效测试1(){

@

班级 {

@

;

无效测试2(){

这两段代码中定义了两个类: 和 、注入的实例、注入的实例,构成了循环依赖。

不过,这不是普通的循环依赖,因为在 test1 方法中添加了 @Async 注解。

猜猜程序启动后会发生什么?

org..beans..: 名称为 '' 的错误 bean:名称为 '' 的 Bean 已作为 a 的一部分进入其原始的其他 beans [] 中,但已被 . 这意味着所说的其他bean 不使用该bean 的final。 这通常是过度渴望的类型 - 使用 '' 并关闭 '' 标志,对于 .

报告了错误。 。 。 原因是循环依赖的发生。

“这不科学,号称可以解决循环依赖问题,为什么还是会出现这种情况?”

如果将上面的代码稍微调整一下:

@

班级 {

@

;

无效测试1(){

去掉test1方法上的@Async注解,两者都需要注入对方的实例,这也构成了循环依赖。

但重启项目发现运行正常。 为什么是这样?

带着这两个问题,让我们一起开始探索循环依赖的旅程吧。

2.什么是循环依赖?

循环依赖:说白了就是一个或多个对象实例之间存在直接或间接的依赖关系。 这种依赖关系就构成了循环调用。

第一种情况:你依赖的是你自己的直接依赖

第二种情况:两个对象之间的直接依赖

第三种情况:多个对象之间的间接依赖

前两种情况的直接循环依赖比较直观,容易识别,但第三种间接循环依赖有时很难识别,因为业务代码调用层次很深。

3、循环依赖的N种场景

循环依赖主要发生在以下场景:

单例注入

这种注入方式应该是最常用的。 代码如下:

@

班级 {

@

;

无效测试1(){

@

班级 {

@

;

无效测试2(){

这是一个经典的循环依赖,但是可以正常运行。 感谢它的内部机制,我们甚至察觉不到它有问题,因为它默默地为我们解决了。

内部有三级缓存:

一级缓存用于保存已经实例化、注入、初始化的bean实例。

s 二级缓存,用于保存已经实例化的bean实例

三级缓存用于保存bean创建工厂,以便以后的扩展有机会创建代理对象。

下图展示了如何解决循环依赖:

图1

细心的朋友可能会发现,二级缓存在这种场景下用处不大。

那么问题来了,为什么要使用二级缓存呢?

试想一下,如果出现以下情况,我们该如何处理呢?

@

班级 {

@

;

@

;

无效测试1(){

@

班级 {

@

;

无效测试2(){

@

班级 {

@

;

无效测试3(){

取决于并且,并且取决于,并且还取决于。

按照上图的流程,就可以向三级缓存注入实例并从三级缓存中获取实例。

假设不使用二级缓存,注入过程如下:

图2

注入需要从三级缓存中获取实例,而三级缓存中存储的并不是真正的实例对象,而是对象。 说白了,两次从三级缓存获取的对象都是对象,通过它创建的实例对象可能每次都不一样。

这不是问题吗?

为了解决这个问题,引入了二级缓存。 在上图1中,对象的实例实际上已经被添加到了二级缓存中,注入时只需要从二级缓存中获取对象即可。

图3

还有一个问题,为什么我们需要将对象添加到三级缓存中呢? 不是可以直接保存实例对象吗?

答:不能,因为如果要增强添加到三级缓存的实例对象,直接使用实例对象是行不通的。

遇到这种情况该如何处理呢?

答案就在类方法中的这段代码中:

它定义了一个匿名内部类,并通过e方法获取代理对象。 其实底层是通过ator类的e来生成代理对象的。

注入多个实例

这种注入方式偶尔会出现,尤其是在多线程场景下。 具体代码如下:

@范围(理论。)

@

班级 {

@

;

无效测试1(){

@范围(理论。)

@

班级 {

@

;

无效测试2(){

很多人说这样的话启动容器就会报错。 这其实是错误的。 我很负责任的告诉你,程序可以正常启动。

为什么?

其实答案在类方法中就已经告诉了。 它会调用该方法。 该方法的作用是在容器启动时提前初始化一些bean。该方法内部调用了tons方法

在红色区域可以清楚地看到:只有非抽象、单例和非延迟加载类可以提前初始化bean。

具有多个实例(即类型)的类不是单例,它们的bean不会提前初始化,因此程序可以正常启动。

如何让他提前初始化bean呢?

你只需要定义一个单例类并将其注入到其中

@

班级 {

@

;

重新启动程序,执行结果为:

豆子在:有吗?

果然出现了循环依赖。

注意:这个循环依赖问题无法解决,因为它不使用缓存,每次都会生成一个新对象。

构造函数注入

这种注入方式现在已经很少使用了,但是我们还是需要了解一下,看下面的代码:

@

班级 {

( ) {

@

班级 {

( ) {

运行结果:

豆子在:有吗?

存在循环依赖,为什么呢?

从图中的流程可以看出,构造函数注入未能添加到三级缓存,并且没有使用缓存,因此无法解决循环依赖问题。

单例代理对象注入

这种注入方法其实很常见。 例如,在使用@Async注解的场景下,会通过AOP自动生成一个代理对象。

我同事的情况也是如此。

@

班级 {

@

;

@异步

无效测试1(){

@

班级 {

@

;

无效测试2(){

从上一篇文章了解到,启动程序时会报错,并且会出现循环依赖:

org..beans..: 名称为 '' 的错误 bean:名称为 '' 的 Bean 已作为 a 的一部分进入其原始的其他 beans [] 中,但已被 . 这意味着所说的其他bean 不使用该bean 的final。 这通常是过度渴望的类型 - 使用 '' 并关闭 '' 标志,对于 .

为什么会出现循环依赖呢?

答案就在下图中:

说白了,bean初始化完成后,还有一步要检查:二级缓存和原始对象是否相等。 由于对前面的流程不重要,所以前面的流程图就省略了,但这里是重点,我们重点说一下:

同事的问题正好是到了这段代码的时候发现二级缓存不等于原来的对象,所以抛出了循环依赖异常。

如果此时将名称更改为:,则其他一切保持不变。

@

班级 {

@

;

@异步

无效测试1(){

重新启动程序,它神奇地就好了。

什么? 为什么是这样?

这从 bean 加载顺序开始。 默认按照文件的完整路径递归查找,按路径+文件名排序,第一个先加载。 所以先加载Bi,改文件名后先加载Bi。

为什么先加载就可以了?

答案就在下图中:

这种情况下,二级缓存实际上是空的,不需要从原始对象来判断,因此不会抛出循环依赖。

循环依赖

还有一个比较特殊的场景。 例如,我们需要在实例化Bean A之前实例化Bean B,这种情况下,我们可以使用@注解。

@(值=“”)

@

班级 {

@

;

无效测试1(){

@(值=“”)

@

班级 {

@

;

无效测试2(){

程序启动后,执行结果为:

- '' 和 ''

在这个例子中,如果 和 都没有用 @ 注解的话,是没有问题的,但是加上这个注解就会导致循环依赖问题。

为什么是这样?

答案就在类方法中的这段代码中:

它将检查实例是否具有循环依赖,如果存在循环依赖则抛出异常。

4、如何解决循环依赖问题?

如果项目中存在循环依赖问题,则说明是默认无法解决的循环依赖。 要看项目的打印日志,看看是哪种循环依赖。 目前包括以下几种情况:

生成代理对象生成的循环依赖

这类循环依赖问题的解决方案有很多,主要有:

使用@Lazy注解延迟加载

使用@注解来指定加载顺序。

修改文件名,改变循环依赖类的加载顺序

使用@生成的循环依赖

这类循环依赖问题可以通过找到循环依赖所在的@注解,强制其不存在循环依赖来解决。

多实例循环依赖

这种类型的循环依赖问题可以通过将bean更改为单例来解决。

构造函数循环依赖

这种类型的循环依赖问题可以通过使用@Lazy注解来解决。