Spring中Bean的创建跟循环依赖

在上文的启动流程探索中,我们知道ioc跟bean的创建是在SpringApplication 对象构造好后,执行run()方法时,再具体一点就是refreshContext()方法执行时

java 复制代码
 // 创建`IOC`容器
  context = this.createApplicationContext();
 // 设置一个启动器,设置应用程序启动
 context.setApplicationStartup(this.applicationStartup);
 // 准备IOC容器的基本信息
 this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
 // 刷新IOC容器   
 this.refreshContext(context);
 // 执行刷新后的处理
 this.afterRefresh(context, applicationArguments);

那Bean执行过程中经历了哪些,它们之间的循环依赖又是什么?

Bean的创建过程

一个bean的创建大概分为4个步骤:

  1. createInstance------调用构造函数,创建实例
  2. 注入依赖,property赋值
  3. 回调Aware、postProcessor..
  4. 放入缓存

循环依赖

那所谓的循环依赖是什么意思呢?

是指在创建Bean的第二阶段------注入依赖时,发现该Bean依赖另一个Bean,而另一个Bean的创建又依赖前一个Bean。从而导致彼此都无法创建的情况。

java 复制代码
 @Service
 public class AService {
 ​
     @Autowired
     private BService bService;
 ​
 }
java 复制代码
 @Service
 public class BService {
 ​
     @Autowired
     private AService aService;
 }

以上代码就是一种典型的情况,当然循环依赖也不只发生在两个Bean之间,多个Bean也可能发生,即使是单个Bean也可能发生------------自己需要自己。

java 复制代码
 @Service
 public class CService {
 ​
     @Autowired
     private CService cService;
 }

缓存结构

对于循环依赖,Spring之所以能解决是因为独特的缓存结构。Spring中的缓存一共有三层:

java 复制代码
 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
 // 一级缓存
 private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
 // 二级缓存
 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
 // 三级缓存

一级缓存:用来存放完全初始化好的Bean,在创建过程中的最后一步,就是放入的这一层。

二级缓存:存放的是半成品的Bean,就是尚未注入依赖的Bean,未进行一些必要的初始化

三级缓存:这里存放的则是BeanFactory(创建Bean的工厂),调用其中的方法getObject()返回半成品的Bean,之后该半成品的Bean会被放入二级缓存中 。在Bean创建的第一阶段完成后,该工厂就会被放入三级缓存中。

如果该过程中Bean的创建顺利,没有出现循环依赖,那么在Bean被创建完成后,在放入一级缓存中后,会将三级缓存中的对象工厂给删除。

使用缓存解决循环依赖

清楚了缓存结构,那么接下来看看它是如何解决bean创建过程中的循环依赖的。就用上面的AService跟BService例子

java 复制代码
 @Service
 public class AService {
 ​
     @Autowired
     private BService bService;
 ​
 }
  1. 调用AService的构造函数进行对象的创建,完成后会放一个ObjectFactory到三级缓存
  2. 之后进行注入依赖(给各个属性赋值),这时发现它依赖BService
  3. 接着去一级缓存 中找是否有BService(之后继续在其他缓存中找),发现没有,于是就会进入BService的创建过程
  4. BService在对象创建后,同样会放一个它的ObjectFactory到三级缓存中
  5. 接着进行BService的依赖注入,发现需要AService,于是进入缓存中找,最终在三级缓存中找到了AServiceObjectFactory,于是调用其getObject方法,得到一个AService的半成品Bean,并将其放入二级缓存中,同时清除三级缓存中的A
  6. 因为AService的依赖已经找到,于是BService的Bean创建成功,因为它创建完成了,此时会将BService的bean放到一级缓存中
  7. 此时BService的bean已经有了,继续AService的bean创建,所有依赖都有了,于是最终AService的bean也会创建成功

为什么需要三级缓存、二级缓存

看了上面的流程,好像只用二级缓存也能解决循环依赖问题,因为只需要一个存放ObjectFactory 的缓存就可以解决循环依赖问题,或者说即使只有一级缓存也能解决循环依赖问题。为什么要设计三级缓存呢?

这是因为最终存放在一级缓存中的bean可能不是第一步中创建的原始实例,有可能是AOP中的代理对象,比如以下这种情况

java 复制代码
 @Service
 public class AService{
   
   @Transactional
   public void test(){
     
   }
 }

因为加了@Transactional所以在对象工厂中生成的就是代理对象,而非第一步中生成的原始对象。所以需要三级缓存,这也是三级缓存存在的主要原因。

那如果把ObjectFactory放入二级缓存呢?放到三级。这是为了延迟加载,因为不是每个bean的生成会导致循环依赖,Spring它考虑的是只有在循环依赖发生的时候,才缓存半成品bean。

同时也是为了提高效率,比如以下这种情况

java 复制代码
 @Service
 public class AService{
   
   @Autowired
   private BService bService;
 }
 ​
 @Service
 public class BService{
   
   @Autowired
   private AService aService;
   @Autowired
   private CService cService;
   
 }
 ​
 @Service
 public class CService{
   
   @AutowireA
   private AService aService;
 }
  • 先创建A发现需要B
  • 接着创建B,发现需要A,于是创建A对象工厂并放入三级,并生成半成品A放入二级
  • 继续注入依赖C,发现没有C
  • 在创建C的过程中,发现它也依赖A,这个时候之前存的二级缓存就起到了作用,可以直接创建

总结一下:

  • 一级缓存则是存放的是实例化完成的bean
  • 二级缓存中的bean有可能是半成品对象,也有可能是代理半成品对象。
  • 三级缓存存的是ObjectFactory,主要是为了AOP的实现

依赖注入扩展

@Autowired 和 @Resource 有什么区别

@Autowired 和 @Resource 都是 Spring/Spring Boot 项目中,用来进行依赖注入的注解。它们都提供了将依赖对象注入到当前bean的功能

  • 来源不同:@Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解

  • 查找顺序不同:

    • @Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找
    • @Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找
  • 支持的参数不同:@Autowired 只支持设置 1 个参数,而 @Resource 支持设置 7 个参数

  • 依赖注入的支持不同

    • @Autowired 既支持构造方法注入,又支持属性注入Setter注入
    • @Resource 只支持属性注入Setter注入

指定构造方法

bean的第一步是对象的构造,spring中bean的默认创建是调用无参构造方法

当然你可以指定构造方法。大家可以用一下简单代码进行实验:

java 复制代码
 @Service
 public class AService {
 ​
     private UserService userService;
 ​
     public AService() {
         System.out.println("bean Aservice 无参构造");
     }
     @Autowired
     public AService(UserService userService) {
         System.out.println("执行有参构造");
         this.userService = userService;
     }
 }

以上代码就是通过@Autowired来指定使用有参构造来创建对象。项目启动时,在控制台便会打印有参构造对应的语句,而不会执行无参构造。

注解@PostConstruct

完成依赖注入后,就进入了第三阶段,执行各种回调。在依赖注入完成后,你如果想对bean做一些操作,比如缓存一些值,或者其他。你可以通过@PostConstruct注解来实现:

java 复制代码
 @Service
 public class AService {
 ​
     private UserService userService;
 ​
     public AService() {
         System.out.println("bean Aservice 无参构造");
     }
 ​
     @PostConstruct
     public void Test(){
         System.out.println("PostConstruct");
     }
 ​
     @Autowired
     public AService(UserService userService) {
         System.out.println("执行有参构造");
         this.userService = userService;
     }
 }
 ​

使用该注解的方法会在依赖注入完成后被自动调用。它的作用是:对当前bean进行一些初始化操作。调用顺序如下:

  • Constructor >> @Autowired >> @PostConstruct

InitializingBean接口

除了通过注解的方式可以实现:自定义方法,在依赖注入完成后自动调用外,还可以用实现接口的方式。在bean类中实现接口InitializingBean,并重写afterPropertiesSet()方法:

java 复制代码
 @Service
 public class AService  implements InitializingBean {
 ​
     private UserService userService;
 ​
     public AService() {
         System.out.println("bean Aservice 无参构造");
     }
 ​
     @Override
     public void afterPropertiesSet() throws Exception {
         System.out.println("initializing ---");
     }
 }
 ​

此时在AService完成依赖注入后,便会执行afterPropertiesSet方法

注意

Spring Boot 2.6.0之后,如果程序中存在循环依赖问题,启动上就会失败,报错

如果要启动,需要进行一些设置

properties 复制代码
 spring.main.allow-circular-references = true
相关推荐
yuanbenshidiaos1 小时前
c++---------数据类型
java·jvm·c++
向宇it1 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
Lojarro1 小时前
【Spring】Spring框架之-AOP
java·mysql·spring
莫名其妙小饼干1 小时前
网上球鞋竞拍系统|Java|SSM|VUE| 前后端分离
java·开发语言·maven·mssql
isolusion1 小时前
Springboot的创建方式
java·spring boot·后端
Yvemil72 小时前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
zjw_rp2 小时前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob2 小时前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder2 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎