渐进分析Bean的循环依赖问题:依赖注入方式,三层缓存,lazy注解

什么是循环依赖问题

  • 就是有两个或两个以上的Bean互相引用(依赖)对方,造成了循环闭环
  • 出现循环依赖问题时尽量想办法消除循环依赖,消除不了再想办法进行规避处理

规避处理

  • 有两种:
    1. Spring自动处理,三层缓存,需要开启配置
    2. @Lazy注解,需要添加在属性上或是构造函数上,而不是类上。

Spring 自动处理

开启循环依赖的支持

//在Spring boot配置文件设置
spring.main.allow-circular-references: true

注:Spring Boot 2.6.0之后,默认为false,即不支持循环依赖,这种情况下,出现循环依赖就会报错如下错误:
请添加图片描述

不可以解决的场景

  1. 多例注入
  2. 依赖双方都为构造器注入
  • 依赖注入分类:
    1. 单例和多例
    2. 属性注入(Field注入), Setter注入,构造器注入
单例与多例
  • 单例:

    1. 实例化一次,所有对该bean的调用都会使用这唯一实例。
    2. Spring容器管理该实例,容器在该实例就在。
  • 多例:

    1. 对该bean的每次调用都会创建一个新的bean实例。
    2. Spring容器负责实例的创建,调用方调用后,容器就不管了,由调用方进行该实例生命周期的维护。
  • Bean的单例和多例是通过scope属性或Scope注解设置的,默认为单例。

    1. XML文件创建Bean时使用属性设置多例
    <bean id="UserService" class="com.service.HelloSerevice" scope="prototype" />
    
    1. 注解方式创建bean时使用Scope注解设置多例
@Component
@Scope("prototype")
public class UserService{
}
依赖注入方式
属性注入/Field注入
public class UserController{
	@Autowired
	//@Resource  @Inject
	private UserService userService;
}

优点:简单,所以最常用。
补充:
1. @Autowired是Spring提供的注解,默认是按照byType的方式注入,要使用byName方式,需要添加注解@Qualifiler
2. @Resource是JDK提供的,默认按照byName的方式注入,其提供两个属性,name和type供选择。
3. @Inject是JDK提供的,默认按照ByType方式注入。

Setter注入
public class SimpleMovieLister { 
	
	private MovieFinder movieFinder; 
	
	public void setMovieFinder(MovieFinder movieFinder) { 
		this.movieFinder = movieFinder;
	} 
}
  • 优点:
    1. 重新注入:即在运行时修改,重新注入参数。
构造器注入
public class SimpleMovieLister { 

	private final MovieFinder movieFinder; 
	
	SimpleMovieLister(MovieFinder movieFinder) { 
		this.movieFinder = movieFinder; 
	} 
}
  • 这是Spring官方推荐的构造器,其有以下优点:

    1. 注入的组件不可变
    2. 确保依赖项不为空
    3. 构造器注入的组件是完全初始化的状态。
  • 官网上对于setter和构造器注入方式的对比。

    1. 更推荐构造器注入。
    2. 构造器适用于强制依赖注入。setter使用于可选依赖注入(当添加@Autowired注解后就变成了必需的依赖注入。不需要一次性提供所有依赖。
    3. setter注入需要进行非空检查。
    4. setter注入支持对象的动态重新配置。

实现方式-三级缓存

Spring Bean的创建流程
  • doCreateBean()方法 主要分为3步
    1. createBeanInstance()实例化,调用对象的构造方法实例化对象。
    2. populateBean()填充属性,如类中的使用@Autowired注解的bean对象。循环依赖发生在这一步
    3. initializeBean()初始化。
使用缓存解决循环依赖问题。

假设A,B类互相依赖注入。

  • 创建A的时候,createBeanInstance(A) -> populateBean(A) 这个时候需要获取Bean B了,BcreateBeanInstance(B) -> populateBean(B)也需要获取BeanA, 就造成循环了。
  • 那么如果我们在创建A第一步的时候就将半成品A(createBeanInstance(A)结果)放在缓存中,创建B的时候调用这个半成品A,就没有循环了。
为什么多例和构造器注入不能被解决。
  • 构造器注入
    1. 在非构造方法中,半成品bean的生成即实例化,是通过反射调用无参构造方法的,现在自己定义了构造方法就不行了。
    2. 这种情况下,在生成半成品之前就会创建依赖的bean,那么依赖的bean就会找不到缓存,造成循环。
    3. 也就是说一个是构造器注入,另一个是非构造器注入是可以的,系统可以先创建非构造器注入的bean,生成缓存,再创建构造器注入的bean。
  • 多例情况下B不会从缓存中获取A,而是会创建一个全新的A,所以会进入死循环。
什么是三层缓存
  • 通过上述分析,我们知道了使用缓存保存bean的早期对象(没有进行属性填充)就可以解决循环依赖问题了。
  • 但实际上,Spring采用的是3级循环:
    1. 第一级缓存: singletonObjects:用于存放完全初始化好的bean。必不可少
    2. 第二级缓存:earlySingletonObjects:用于存放原始的bean对象,即半成品bean或半成品bean的代理对象(没有填充属性)
    3. 第三级缓存:singletonFactories: 存放bean工厂对象,存放半成品bean。
循环依赖时3层缓存的使用流程。
  • 普通循环依赖:
    1. a实例化完成之后(a成为半成品),将a放入三级缓存
    2. 给a填充属性b,又去创建b
    3. b实例化完成之后(b成为半成品),将b放入三级缓存
    4. 给b填充属性a,又去从容器中获取a
    5. 此时可以从三级缓存中查到a。将半成品a放入二级缓存,并从三级缓存中移除a。最终返回半成品a。开始回溯。
    6. b的创建过程回溯完之后,b成为正品,将b从三级缓存中移除,将b放入一级缓存。而a还是半成品。(b中的属性a的属性b还是null,即:b.a.b=null)
    7. 将b返回给a,然后a进行第三步初始化。,a也成为正品,将a放入一级缓存,并从二级缓存中移除。循环依赖已解决,a和b均创建成功。

注:半成品bean会先放在三级缓存中,再被使用时会放在二级缓存,变成成品时会放入一级缓存

  • 增加AOP之后:
    5. 此时从从三级缓存中查到a,然后生成a的代理对象,将a的代理对象放在第二级循环中
    6. b填充后属性之后,初始化时创建b的代理对象,将b从3层缓存中删除,然后将b的代理对象放入一级缓存
    7. b创建完后返回给a,然后a进行初始化,将a的代理对象从二级缓存中删去,然后放在一级缓存中。

注:这时候A的代理对象是在a还是半成品的时候进行的B的代理对象是在b变成成品的时候创建的

非要3级缓存吗
  • 没有AOP,那么只需要两级就够了

    1. 第一级必不可少,存放完全初始化好的bean。
    2. 第二级存放半成品bean就可以了。不需要3层
  • 有AOP功能,那么需要3级

    1. 第一级依旧必不可少
    2. 第二级存放代理对象。
    3. 第三级存放半成品bean。
  • 有AOP,也可以通过设计不需要第三级缓存,那就是将b的代理对象也提前,在其是半成品时进行创建。 但Spring没有这样做。因为其原则是代理对象尽量推迟创建。

@lazy解决循环依赖

  • lazy可以解决循环依赖,但其不能放在类名上。而应该放在依赖注入的地方。如:
@Lazy  
A(C b){  
  this.b = b;  
}

@Lazy  
@Autowired  
private A a;

  • lazy可以解决依赖双方都为构造器注入的场景
  • lazy可以解决多例情况
  • 原理: 就是加了Lazy注解后,创建bean A的时候就不会创建依赖的bean B,而是在使用的时候再创建,这个时候A已经创建好了,所以就不会出现循环依赖。
  • lazy是通过动态代理完成的,创建bean A的时候就不会创建依赖的bean B指的是创建A的时候只会创建B的代理对象,不会触发注入对象的加载。

参考文献

[Spring中Bean的单例和多例简单总结]https://blog.csdn.net/u014644574/article/details/103308742
https://cloud.tencent.com/developer/article/1497692
https://www.cnblogs.com/lvdeyinBlog/p/15178226.html
https://juejin.cn/post/7301242196887027721
https://blog.csdn.net/qq_43290318/article/details/114679612
Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗 - 青石路 - 博客园 (cnblogs.com)
【Spring源码三千问】@Lazy原理分析——它为什么可以解决特殊的循环依赖问题?_@lazy为什么能解决循环依赖-CSDN博客

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值