Spring如何通过@Lazy注解解决构造方法循环依赖问题

 更新时间:2023年03月29日 15:12:07   作者:魔道不误砍柴功  
循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环,这篇文章主要给大家介绍了关于Spring如何通过@Lazy注解解决构造方法循环依赖问题的相关资料,需要的朋友可以参考下

什么是循环依赖?

先定义两个类 Apple、Orange,如下所示:

@Component
public class Apple{

	@Autowired
	private Orange orange;
}

@Component
public class Orange {

	@Autowired
	private Apple apple;
}

像这种在 Apple 里面有一个属性 Orange、Orange 中有一个属性 Apple,你中有我,我中有你,这样可以称之为循环依赖。循环依赖问题不止在 Spring 中有,在 Mybatis 中也有,解决思想基本一样,都需要借助额外的缓存进行实现。

Spring 对于这种属性注入的循环依赖是支持的,不会有任何问题,今天这里探讨一下 Spring 中构造方法的循环依赖问题,Spring 默认是不支持的,但是也提供了方法解决。

构造方法循环依赖

同样把上面 Apple、Orange 两个类改造下,如下所示:

@Component
public class Apple{

	public Apple(Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
	}
}

@Component
public class Orange {

	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

测试类如下:

public class TestCircleMain {

	public static void main(String[] args) {

		ApplicationContext context = new AnnotationConfigApplicationContext(CircleConfig.class);

		Orange orange = context.getBean( Orange.class);

		Apple bean = context.getBean(Apple.class);
	}

}

发现直接就抛出了循环依赖异常如下:

很显然构造方法的循环依赖,Spring 是不太支持的,但是我非要这样使用,怎么解决呢?

加 @Lazy 注解解决构造方法循环依赖

具体怎么解决构造方法循环依赖问题呢?可以通过加 @Lazy 注解,但是也需要注意一些细节(后面我们会分析到),这里先看加 @Lazy 注解之后能够解决构造方法循环依赖的案例,如下所示:

@Component
public class Apple{
    @Lazy
	public Apple(Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
	}
}

@Component
public class Orange {
	
	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

或者加载参数上也行,都表示一个意思,就是后面会临时创建出 Orange 的代理对象

@Component
public class Apple{
    
	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
	}
}

@Component
public class Orange {
	
	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

加上之后测试结果正常输出,如下:

=====> 调用 Apple 构造方法
======>调用 Orange 构造方法

那么为什么加上 @Lazy 注解就能够解决这样一个问题呢?继续往下分析,这里我们主要看加上这个 @Lazy 注解的执行流程是什么样的?

@Lazy 执行流程源码分析

首先第一次过来的是 Apple 类,先从缓存中查询是否有实例化的对象,源码如下:

第一次过来很显然是没有的,然后就要开始去创建实例,但是创建实例 Spring 会做一个标识,避免重复创建实例,这个标识标识这个 Apple 类正在创建中,当创建成功之后就会删除,此时创建好的实例就会存在缓存中。

记录标识源码如下:注意如果标识 singletonsCurrentlyInCreation 容器中已经存在,那么会直接添加失败,抛出 BeanCurrentlyInCreationException 异常

异常信息也是大家非常熟悉的循环依赖问题,源码如下:

接着要开始创建实例,如下所示:

会选择出一个合适的构造方法进行实例化,由于我们只有且仅有一个构造方法,所以肯定就用这个唯一的构造方法了,然后就开始进入 autowireConstructor() 属性注入环节

拿到构造方法中所有的参数,对每个参数一一遍历进行赋值操作,那么就要格外关注这个方法是怎么做的了

进入 createArgumentArray() 方法内部逻辑,源码如下(核心部分,无关代码省略):

很显然这里是采用 for 循环对构造方法中参数一一赋值,所以构造方法中如果参数过多,性能也会降低许多,这个得注意了

看到 resolveDependency() 方法一定要有一种意识,就是极大可能要出发 getBean() 操作了,除了代理不会触发,但是也不一定(后面会讲到)

然后下面这一段代码是加了 @Lazy 注解的关键处理逻辑了(这段逻辑非常非常重要):

1、就是先判断构造方法中(构造方法上,或者参数上)是否标注了 @Lazy 注解,如果标注了就会创建代理对象,不会立即触发 getBean() 操作
2、反之,就是走正常的逻辑,直接调用 getBean() ,但是这样就会直接报异常了,因为 Spring 是不支持构造方法的循环依赖的(还没有来得及把半 Apple 类的半成品放到三级缓存),只有加了 @Lazy 注解临时通过代理方法可以解决构造方法循环依赖

然后进入 getLazyResolutionProxyIfNecessary() 方法看是怎么判断,和要怎么创建代理对象的,源码如下:

可以清楚的看到 @Lazy 为什么可以标注在构造方法上和构造方法的入参上面两种方式,可以从下面这段源码中知道答案,如下所示:

找到了 @Lazy 注解就会通过 buildLazyResolutionProxy() 方法去创建这个入参的代理对象,如下所示:

代理对象就是对原始目标类的一种增强,注意当使用代理对象调用它的方法时会回调到 getTarget() 方法,这个 getTarget() 方法中调用了 doResolveDependency() 方法,这个方法会触发调用 getBean() 流程实例化 bean,要格外注意,后面会演示如何调用到这个方法的。

代理对象已经创建好了,现在准备通过构造方法反射调用实例化 Apple 类即可,源码如下:

其中 argsWithDefaultValues 值是 Orange 的一个代理对象,实例化好之后相当于 Apple 已经创建好了,然后放入到三级缓存中,如下所示:

然后就是删除 singletonsCurrentlyInCreation 标识容器中的标识位(因为已经实例化完成了,所以标识位可以抹除),如下所示:

最后在把 Apple 实例化好的 bean 从三级缓存中删除,然后移动到一级缓存中,也就是我们经常所说的单例缓冲池中,如下所示:

至此,Apple 类实例化 bean 就已经在 Spring 的单例缓冲池中存在了,其他地方如果想要使用直接从这个单例缓冲池中取值即可。

那么当 Orange 类过来实例化的时候,也是先从容器中查找是否有实例化 bean 存在,源码如下:

然后打标记,源码如下:

异常信息也是大家非常熟悉的循环依赖问题,源码如下:

接着要开始创建实例,如下所示:

会选择出一个合适的构造方法进行实例化,由于我们只有且仅有一个构造方法,所以肯定就用这个唯一的构造方法了,然后就开始进入 autowireConstructor() 属性注入环节

拿到构造方法中所有的参数,对每个参数一一遍历进行赋值操作,那么就要格外关注这个方法是怎么做的了

然后进入代码核心逻辑,此时因为在 Orange 的构造方法中是没有标注 @Lazy 注解的,所以这里不会进入创建代理的逻辑,而知直接进入 doResolveDependency() 逻辑,前面已经提到很多遍历,这个方法很重要,会触发到 getBean() 流程。

那么 Orange 构造方法中的入参为 Apple,Apple 在第一遍的时候就已经在单例缓冲池中存在了,所以 Apple 在执行 getBean() 流程的时候,直接就会从一级缓存中获取到 Apple 实例化好的对象,赋值给 Orange 构造方法中的 Apple
变量。

然后表示就是 Orange 通过反射调用构造方法实例化 Orange 实例

然后后面的流程 Orange 也是要放入到三级缓存中,然后删除标识位,最后将 Orange 实例从三级缓存中删除,移动到一级缓存(单例缓存池)中。

至此 Apple、Orange 两个构造方法的循环依赖就分析完成了,下面是稍微改动一点,继续分析。

使用 @Lazy 注解注意事项(特别小心)

将 Apple、Orange 类稍微变动一下,如下所示:

@Component
public class Apple{

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		System.out.println("======>orange="+orange);
	}
}

@Component
public class Orange {

	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

经过上的分析,Apple 构造方法中 @Lazy 注解修饰的 Orange,会创建一个代理对象来规避入参 orange 调用 getBean() 流程,从而解决循环依赖问题,现在我们在 Apple 构造方法中,直接把 orange 打印出来。

经过测试直接报错,错误如下:

发现还是发生了循环依赖问题,下面具体分析下是为什么呢?前面分析过的下面都会直接通过简短描述直接带过

1、Apple 类首先会去缓存中查找是否已经实例化 bean,第一次很显然没有

2、开始记录标记位

3、调用 createBeanInstance() 方法实例化对象

4、给 Apple 类构造方法的入参进行属性赋值,会创建代理类,如下所示:

注意这里面的 getTarget() 方法,下面会回调到这里,现在代码继续往后走,代理类创建好之后就要开始通过反射调用构造方法创建实例了,源码如下:

注意此时的 argsWithDefaultValues 是 Orange 代理对象,当我们通过反射调用 Apple 的构造方法时,立即回调到 Apple 的构造方法中的逻辑,如下所示:

@Component
public class Apple{

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		System.out.println("======>orange="+orange);
	}
}

先执行输出语句,打印出"=====> 调用 Apple 构造方法",然后再打印下一句语句时,请注意,orange 是一个代理对象,在 JVM 执行这条输出语句的时候,其实默认调用了 toString() 方法,你要知道在创建代理对象的时候,并没有限定哪个方法增强,而是对整个 Orange 中的方法增强了,所以在你输出 Orange 的时候就会触发代理对象的对 toString() 方法的增强,所以会回调到代理对象中的 intercept() 方法,然后再 intercept() 方法中有调用了 getTarget() 方法,注意哦在创建代理对象的时候,我特意说明要注意 getTarget() 方法,因为现在就要被回调到了,恰好 getTarget() 方法中又会触发 getBean() 流程,所以最终又导致循环依赖问题的产生。

对于 Apple 类构造方法中的入参 Orange, Spring 是通过 cglib 进行代理对象创建的,具体看 CglibAopProxy 类就知道为什么在执行 toString() 方法最终会回调到 getTarget() 方法,这里就截取一段核心代码,如下:

那么怎么解决这个问题呢?

1、在 Apple 类构造方法中不要调用任何代理对象的方法,比如这样使用,如下所示:

@Component
public class Apple{

	private Orange orange;

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		this.orange = orange;
	}
	public void sop() {
		System.out.println("this.orange = " + this.orange);
	}
}

@Component
public class Orange {
	private Apple apple;
	public Orange(Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

我只是在 Apple 构造方法中使用了一下 Orange 代理对象,并没有调用任何 API,所以不会触发代理对象执行增强逻辑。

2、继续在 Apple 构造方法中触发代理对象回调(调用 toString() 等方法),此时会出现循环依赖问题,就是因为方法 toString() 的增强逻辑触发了 Orange 的 getBean() 操作,然后 Orange 实例化时,又触发了 Orange 构造方法中的入参 Apple 类的实例化,此时你要知道 Apple 类还没有实例化完成呢,缓存中压根也还没有,Apple 类现还停留在System.out.println("======>orange="+orange); 输出语句呢

所以说到这里了,我们也可以在 Orange 中加上 @Lazy 注解,如下所示:

@Component
public class Apple{

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		System.out.println("this.orange = " + this.orange);
	}
	public void sop() {
		System.out.println("this.orange = " + this.orange);
	}
}

@Component
public class Orange {
	private Apple apple;
	public Orange(@Lazy Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
	}
}

当给 Orange 类构造方法中入参 Apple 赋值先给定一个代理对象,避免 Apple 类触发 getBean() 操作,这样 Orange 构造方法的入参就相当于赋上值,那么 Orange 类就完成了实例化,代码回调上层调用处,就是 Apple 类构造方法中的输出语句 System.out.println("======>orange="+orange); 这条输出语句执行完,相当于 Apple 类构造方法也实例化完成,从而没有发生循环依赖问题。

但是如果在将 Apple、Orange 类变动一下,如下所示:

@Component
public class Apple{

	public Apple(@Lazy Orange orange) {
		System.out.println("=====> 调用 Apple 构造方法");
		System.out.println("this.orange = " + this.orange);
	}
}

@Component
public class Orange {
	private Apple apple;
	public Orange(@Lazy Apple apple) {
		System.out.println("======>调用 Orange 构造方法");
		System.out.println("this.orange = " + this.orange);
	}	
}

这样是绝对没办法解决了,因为相当于 @Lazy 注解没有加上一样,每个构造方法中都会立即触发 getBean() 操作,此时以为缓存中根本还没来得及放入实例化 bean。

以上只是个人对 @Lazy 的理解,仅供参考。

总结

在构造方法循环依赖问题中,通过 @Lazy 注解,只是临时创建一个代理对象来为属性赋值,避免触发二次 getBean() 调用。

并且注意代理对象和被 @Lazy 修饰的类的实例并不是同一个,完全是两个对象,可以输出 hashCode() 编码即可查看,不能使用 toString() 来做验证,因为代理对象会回调到切面逻辑,然后触发 getBean() 实例化 @Lazy 修饰的类,然后最终通过 toString() 方法输出的结果都是一样的 com.gwm.circle.Banana@5149d738,然后你就会误认为代理对象和被 @Lazy 修饰类的真正对象是相同的,其实并不相同,代理对象是代理对象,和真正实例完全是两个对象!

到此这篇关于Spring如何通过@Lazy注解解决构造方法循环依赖问题的文章就介绍到这了,更多相关Spring解决构造方法循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Springboot 如何指定获取自己写的配置properties文件的值

    Springboot 如何指定获取自己写的配置properties文件的值

    这篇文章主要介绍了Springboot 如何指定获取自己写的配置properties文件的值,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • java仿QQ微信聊天室功能的实现

    java仿QQ微信聊天室功能的实现

    这篇文章主要介绍了java仿QQ微信聊天室的实现代码,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,,需要的朋友可以参考下
    2021-04-04
  • java stream distinct()如何按一个或多个指定对象字段进行去重

    java stream distinct()如何按一个或多个指定对象字段进行去重

    这篇文章主要介绍了java stream distinct()如何按一个或多个指定对象字段进行去重问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • Java Swing JComboBox下拉列表框的示例代码

    Java Swing JComboBox下拉列表框的示例代码

    这篇文章主要介绍了Java Swing JComboBox下拉列表框的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • java比较两个json文件的差异及说明

    java比较两个json文件的差异及说明

    这篇文章主要介绍了java比较两个json文件的差异及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • @CacheEvict中的allEntries与beforeInvocation的区别说明

    @CacheEvict中的allEntries与beforeInvocation的区别说明

    这篇文章主要介绍了@CacheEvict中的allEntries与beforeInvocation的区别说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • Java通过递归算法解决迷宫与汉诺塔及八皇后问题

    Java通过递归算法解决迷宫与汉诺塔及八皇后问题

    方法就是用来完成解决某件事情或实现某个功能的办法;程序调用自身的编程技巧称为递归,本文主要讲的是通过递归来实现三个经典的问题,解决迷宫,汉诺塔,八皇后问题,感兴趣的朋友可以参考一下
    2022-05-05
  • Java匿名内部类的写法示例

    Java匿名内部类的写法示例

    这篇文章主要给大家介绍了关于Java匿名内部类的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08
  • Java代码是如何被CPU狂飙起来的

    Java代码是如何被CPU狂飙起来的

    无论是刚刚入门Java的新手还是已经工作了的老司机,恐怕都不容易把Java代码如何一步步被CPU执行起来这个问题完全讲清楚。本文就带你详细了解Java代码到底是怎么运行起来的。感兴趣的同学可以参考阅读
    2023-03-03
  • 轻松理解Java面试和开发中的IoC(控制反转)

    轻松理解Java面试和开发中的IoC(控制反转)

    在Java开发中,IoC意 味着将你设计好的类交给系统去控制,而不是在你的类内部控制。这称为控制反转。下文给大家介绍Java面试和开发中的IoC(控制反转)知识,需要的朋友参考下吧
    2017-07-07

最新评论