Springboot Bean创建流程、三种Bean注入方式(构造器注入、字段注入、setter注入)、循坏依赖问题

文章目录

    • [1 Bean 创建流程](#1 Bean 创建流程)
      • [1.1 Bean的扫描注册](#1.1 Bean的扫描注册)
      • [1.2 创建Bean的顺序](#1.2 创建Bean的顺序)
    • [2 三种Bean注入方式](#2 三种Bean注入方式)
      • [2.1 构造器注入 | Constructor Injection(推荐)](#2.1 构造器注入 | Constructor Injection(推荐))
      • [2.2 字段注入 | Field Injection(常用)](#2.2 字段注入 | Field Injection(常用))
      • [2.3 方法注入 | Setter Injection](#2.3 方法注入 | Setter Injection)
      • [2.4 三种方式注入顺序](#2.4 三种方式注入顺序)
    • [3 循环依赖](#3 循环依赖)
      • [3.1 构造器注入](#3.1 构造器注入)
      • [3.2 字段/setter注入](#3.2 字段/setter注入)
      • [3.3 乱想:构造器+字段?](#3.3 乱想:构造器+字段?)
      • [3.4 解决方案](#3.4 解决方案)

1 Bean 创建流程

简单来说,当容器里要放的Bean很多时,Spring会优先创建依赖最少的Bean。

1.1 Bean的扫描注册

Spring启动后首先会根据SpringbootApplication的包扫描配置扫描包里的所有文件,然后将使用了注解标记的类(如@Component、@Service、@Repository、@Configuration)作为组件注册到上下文中,同时解析这些组件之间的依赖关系。

1.2 创建Bean的顺序

组件之间的依赖关系通常会使用图/树结构来表示,如果BeanA依赖BeanB,那么BeanA是BeanB的父节点。在创建Bean的时候,Spring会优先选择树的叶子结点进行创建,因为它不存在依赖,然后再不断向上层进行创建,也就是自底向上创建,这样才能尽量确保在创建某个Bean时,它依赖的Bean已经存在。

在创建Bean的时候,Spring仍会检查它需要的依赖是否已经存在,如果存在则直接注入,如果依赖Bean还没创建,那么会去递归创建依赖的Bean,直到所有依赖都被创建,再创建当前Bean。

2 三种Bean注入方式

2.1 构造器注入 | Constructor Injection(推荐)

构造器注入是在组件的构造函数中注入所需的依赖,它是在Bean创建时就注入依赖,创建流程如1.2。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class BeanA {
    private final BeanB beanB;
    private final BeanC beanC;

    public BeanA(BeanB beanB, BeanC beanC) {
        this.beanB = beanB;
        this.beanC = beanC;
    }
    // 其他方法...
}

@Component
public class BeanB {
    // 其他方法...
}

@Component
public class BeanC {
    // 其他方法...
}

使用构造器进行依赖注入时,依赖的对象通常会被声明为final,这样当对象创建后,依赖的Bean不会被改变,可以保证类的一致性。

这样注入的优势是能使Bean之间的依赖关系更加清楚,避免了字段注入可能存在的隐式依赖,如果存在问题(比如循环依赖)会在Spring初始化时就抛出异常,而不会等到执行时才出错。

需要注意的是不能显示提供无参构造函数 ,否则Spring会优先执行无参构造,导致所有依赖的Bean都为null,如果有多个构造函数,选择一个使用@Autowired注解,否则可能报错。

2.2 字段注入 | Field Injection(常用)

字段注入就是使用@Autowired注解自动注入依赖的Bean。它不会在构造函数中注入,而是通过反射在组件构造函数执行后才注入依赖Bean即Bean实例化完成后才注入依赖项)因此不能使用final修饰依赖Bean,因为使用final字段修饰的变量必须在声明时或在构造函数中初始化,而字段注入在构造函数之后执行。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class BeanA {
	@Autowired
    private BeanB beanB;
}

@Component
public class BeanB {
    // 其他方法...
}

字段注入是开发时最常用的方式,但由于字段注入是在Bean实例后才注入,属于隐式依赖,所以可能会存在空指针问题,而这个问题只有当程序运行时才出现,因此有一定隐患,所以Spring官方更推荐构造器注入。

2.3 方法注入 | Setter Injection

和字段注入类似,只是需要写好一个setter函数,在setter中注入依赖,一个setter方法通常对应一个依赖,@Autowired注解写在setter方法上:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class BeanA {
    private final BeanB beanB;
    private final BeanC beanC;
    
	@Autowired
    public setBeanB(BeanB beanB) {
        this.beanB = beanB;
    }
    
    @Autowired
    public setBeanC(BeanC beanC) {
        this.beanC = beanC;
    }
   

setter注入的优势是可以灵活注入bean,相比构造器一次性写入更加清晰一些,缺点和字段注入类似。

2.4 三种方式注入顺序

如果一个组件中同时存在以上三种注入方式,执行顺序是?

按照构造器注入-->字段注入-->方法注入的原则执行。

首先执行构造函数,注入在构造函数初始化的Bean。构造函数执行结束后,Spring将处理字段注入,然后在容器中查找并注入依赖Bean。最后如果存在带有@Autowired注解的setter方法,Spring会再调用这些方法注入依赖。

3 循环依赖

循环依赖指的是两个Bean之间互相需要对方作为成员变量,如BeanA需要注入BeanB,BeanB需要注入BeanA。

3.1 构造器注入

构造器注入时会通过构造函数注入所有必须的依赖,当两个组件BeanA和BeanB之间存在循坏依赖时,执行BeanA的构造函数需要注入BeanB(此时BeanA还未创建),由于BeanB还未生成,因此转而先创建BeanB,执行BeanB的构造函数,而BeanB同样需要注入BeanA,于是出现了死锁情况,两个Bean都无法创建,因此如果使用构造器注入而又出现循环依赖时,Spring会直接抛出BeanCurrentlyInCreationException异常。

3.2 字段/setter注入

使用字段/setter注入在循环依赖时不会抛出异常(但很容易出问题),主要是通过提前暴露Bean的方式来解决的。

因为在循环依赖的情况下,字段/setter注入会先创建当前对象的"代理"或"占位符"实例/引用(非完全实例,但可以拿来使用) ,然后通过反射等依赖对象存在后再注入依赖,因此不会在创建时出现死锁(没有循环等待),而构造器注入必须注入完全初始化的依赖后才能实例化,因此会死锁。

以BeanA/B为例:

  1. 当容器尝试创建BeanA时候,发现它依赖BeanB,然后转而去创建BeanB。
  2. 创建BeanB时,发现其依赖于BeanA,循环依赖出现,因此会在单例池中创建一个BeanA的占位符实例(未初始化)。
  3. BeanB创建后,Spring会将BeanA的占位符注入到BeanB中,此时BeanB完成注入,它的实例也创建完毕,将被放入单例池。
  4. 返回BeanA的创建,此时BeanA已经有占位符实例,BeanB也有实例,因此可以将BeanB注入BeanA中。
  5. BeanA和BeanB都完成初始化。

占位符实例并不是在 BeanA 开始创建时就生成的,而是依赖关系的解析过程中,当需要 BeanA 时才创建。因为当Spring开始创建一个Bean的时候会标记当前Bean为"正在创建",如果在它的实例化过程中(递归注入依赖)发现有其他对象请求该bean,则证明循环依赖出现,Spring正是通过这种动态追踪的方式来识别循环依赖的。

如果不存在循环依赖,BeanB已经实例化那么会被直接注入BeanA,这样BeanA也会直接完成初始化实例。没有循环依赖不会生成占位符实例。

3.3 乱想:构造器+字段?

想到了一个组合:如果BeanA使用构造器注入BeanB,而BeanB使用字段注入注入BeanA,那么能否通过占位符实例实现注入呢?

答案是不行,原因主要有三点:

  1. 构造器注入要求注入的依赖必须是完全初始化的实例【核心】。
  2. 构造器注入时,在循环依赖情况下被动生成的占位符实例不允许使用(因为构造函数不允许注入未完全实例化的对象,本质上与第二点一样)。
  3. 构造器注入不会像字段注入那样生成占位符实例,因为就算生成了也不完全,无法使用

因此无论是先创建BeanA还是先创建BeanB都会抛出异常。

先创建BeanA:发现依赖BeanB,转而创建BeanB,又发现BeanB依赖BeanA,因此尝试创建BeanA的占位符实例,但是因为A是构造器注入,必须注入BeanB的完整实例(但并不存在),因此不允许使用占位符实例,失败。

先创建BeanB:发现依赖BeanA,转而创建BeanA,BeanA必须使用完全实例化的BeanB,不会创建BeanB的占位符实例,因此无法达成,失败。

3.4 解决方案

一般情况下需要避免循环依赖,如果存在,可以尝试将一些依赖关系移除,重构依赖关系,降低耦合。或者可以使用@Lazy注解延迟Bean的加载,懒加载可以让Bean在被使用时才注入。

java 复制代码
@Component
public class BeanA {

    @Autowired
    @Lazy
    private BeanB beanB; // 延迟注入

    // 其他方法...
}

@Component
public class BeanB {

    @Autowired
    private BeanA beanA; // 直接注入

    // 其他方法...
}
相关推荐
Mos_x17 小时前
计算机组成原理核心知识点梳理
java·后端
墨寒博客栈17 小时前
Linux基础常用命令
java·linux·运维·服务器·前端
回忆是昨天里的海17 小时前
k8s-部署springboot容器化应用
java·容器·kubernetes
INFINI Labs17 小时前
使用 Docker Compose 轻松实现 INFINI Console 离线部署与持久化管理
java·docker·eureka·devops·docker compose·console·easyserach
立早正文17 小时前
Docker从零到一部署DNMP+Redis《全程干货》
docker·容器·php
Cosolar17 小时前
国产麒麟系统 aarch64 架构 PostgreSQL 15 源码编译安装完整教程
java·后端
GalaxyPokemon17 小时前
PlayerFeedback 插件开发日志
java·服务器·前端
hkNaruto17 小时前
【k8s】Kubernetes 资源限制设置规范手册 MB与MiB的概念混淆问题
云原生·容器·kubernetes
天天摸鱼的java工程师18 小时前
别再写那些重复代码了!8年Java老兵教你用 Hutool 提升开发效率
java·后端
喝杯绿茶18 小时前
springboot中的事务
java·spring boot·后端