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

devtools/2025/2/13 13:43:20/
文章目录
    • 1 Bean 创建流程
      • 1.1 Bean的扫描注册
      • 1.2 创建Bean的顺序
        • 1.2.1 存在依赖关系
        • 1.2.2 不存在依赖关系
    • 2 三种Bean注入方式
      • 2.1 构造器注入 | Constructor Injection(推荐)
      • 2.2 字段注入 | Field Injection(常用)
      • 2.3 方法注入 | Setter Injection
      • 2.4 三种方式注入顺序
    • 3 循环依赖
      • 3.1 构造器注入
      • 3.2 字段/setter注入
        • 3.2.1 三级缓存实现
        • 3.2.2 三级缓存的工作原理:
        • 3.2.3 具体例子
        • 3.2.4 为何要第三级缓存ObjectFactory?
      • 3.3 乱想:构造器+字段?
      • 3.4 解决方案

1 Bean 创建流程

简单来说,当容器里要放的Bean很多时,Spring会优先创建依赖最少的Bean。本文主要考虑单例模式。

1.1 Bean的扫描注册

Spring启动后首先会根据SpringbootApplication的包扫描配置扫描包里的所有文件,然后将使用了注解标记的类(如@Component、@Service、@Repository、@Configuration等)和xml文件里定义的Bean生成BeanDefinition对象注册到上下文ApplicationContext中,该对象包括Bean的名称、类型、作用域、构造函数参数、依赖等信息。

1.2 创建Bean的顺序
1.2.1 存在依赖关系

Spring在创建Bean之前会先通过BeanDefinition分析Bean之间的依赖关系,通常使用有向无环图DAG构建,若BeanA依赖BeanB那么会有一条由BeanB指向BeanA的有向边。根据DAG获得拓扑排序,优先对入度为0(或最少)的Bean节点初始化,这样才能尽量确保在创建某个Bean时,它依赖的Bean已经存在。

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

1.2.2 不存在依赖关系

对于不存在依赖关系的Bean,Spring会按照以下顺序加载Bean:

  • XML配置文件里按照Bean的定义顺序加载;
  • @ComponentScan及其子注解(@Component、@Service、@Repository)声明的Bean按照字母顺序加载;
  • 配置类中@Import按照导入的顺序加载;
  • 配置类@Bean方法定义的Bean按照方法的声明顺序加载。

如果要人为控制Bean的加载顺序,可以使用@order@DependsOn注解。

2 三种Bean注入方式

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

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

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字段修饰的变量必须在声明时或在构造函数中初始化,而字段注入在构造函数之后执行。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class BeanA {@Autowiredprivate BeanB beanB;
}@Component
public class BeanB {// 其他方法...
}

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

2.3 方法注入 | Setter Injection

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component ![qwqw](https://i-blog.csdnimg.cn/direct/28c3f979e67f4abab03bc2ab850b2837.png#pic_center)
public class BeanA {private final BeanB beanB;private final BeanC beanC;@Autowiredpublic setBeanB(BeanB beanB) {this.beanB = beanB;}@Autowiredpublic 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的方式解决的。

一般的Bean需要通过实例化(构造器)、属性填充(注入依赖)、初始化(执行Bean的init方法)才能完成初始化。而在循环依赖的情况下,字段/setter注入会在实例化后先创建当前对象的“代理”或“占位符”实例/引用(半成品实例,但可以被注入到其他Bean中),这个实例可以通过反射等它依赖的对象存在后再进行属性填充,因此不会在创建时出现死锁(没有循环等待),而构造器注入必须注入_完全初始化_的依赖后才能实例化,因此会死锁。

3.2.1 三级缓存实现

三级缓存是解决循环依赖的关键,spring的三级缓存实现在DefaultSingletonBeanRegistry 类中,主要有三个Map,分别是:

  • singletonObjects,一级缓存,用于存放完全初始化完成的单例Bean,可以被其他Bean直接引用
  • earlySingletonObjects,二级缓存,用于存放提前暴露,即未完全初始化的半成品Bean,目的是为了解决循环依赖
  • singletonFactories,三级缓存,用于存放ObjectFactory对象,该对象可以创建半成品Bean放入二级缓存。三级缓存主要用于解决AOP代理对象。
3.2.2 三级缓存的工作原理:

当需要注入某个Bean时,spring通过如下顺序到三级缓存中找:

  1. 检查一级缓存是否存在完全初始化的Bean,若有则返回;
  2. 一级缓存未找到且该Bean正在创建中则继续检查二级缓存,否则返回null。二级缓存保存的是半成品Bean,在循环依赖时才用到,若有也直接返回;
  3. 二级缓存未找到且允许早期依赖则去三级缓存找,否则返回null。如果查询到对象工厂,则会调用工厂的 getObject() 方法生成Bean的早期引用然后将其存入二级缓存,同时将工厂从三级缓存中移除;
  4. 当Bean的生命周期完成后(注入依赖,初始化函数回调),spring会将完全体Bean放入一级缓存,同时移除二三级缓存中的该Bean。
3.2.3 具体例子

以BeanA/B为例:

  1. 当容器尝试创建BeanA时候,发现它依赖BeanB,但此时在缓存中未存在BeanB,因此会先创建BeanA的ObjectFactory对象放入三级缓存中并标记BeanA为正在创建中,然后转而去创建BeanB;
  2. 创建BeanB时,发现其依赖于BeanA,且能够在三级缓存中发现BeanA的工厂对象,那么会使用工厂创建BeanA的半成品对象放入二级缓存,然后清除BeanA在三级缓存中的工厂;
  3. 半成品BeanA返回给BeanB完成注入,打破循环依赖,BeanB继续初始化生成完全体BeanB放入一级缓存,二三级缓存清除BeanB;
  4. BeanB创建后返回BeanA完成注入,A继续初始化生成完全体BeanA放入一级缓存,二三级缓存清除BeanA;
  5. BeanA和BeanB都完成初始化。
3.2.4 为何要第三级缓存ObjectFactory?

在上面的Bean创建流程中可以看到,解决循环依赖时最终还是从二级缓存中找到半成品Bean,那既然如此,为什么在递归创建依赖对象时,不直接先生成当前对象的半成品Bean放入二级缓存,而是先创建当前Bean的工厂对象呢?

这是因为二级缓存只是看似能解决循环依赖,但是存在着以下问题:

  1. 可能创建多余的半成品对象:若没有三级缓存保存工厂对象,那么在创建某Bean并在递归创建依赖Bean之前就应该创建好半成品Bean,以解决可能存在的循环依赖,但是循环依赖并不是一定存在的,这种方式在无循环依赖的情况下会创建很多无用的半成品Bean,因为它们本来都可以正常初始化直接生成完全Bean。
  2. 难以支持代理Bean创建:如果某个类使用了AOP特性(或事物),那么这个类被注入时应该使用它的代理对象。若只使用二级缓存,那么要么生成Bean的原始半成品对象,要么生成Bean的代理半成品对象,但无论是哪种模式都不合适,因为前者无法满足代理需求,后者会使得普通对象也注入代理对象,造成不必要的开销。

正是因为这些问题,spring才采取了三级缓存,工厂对象的引入可以很好解决上面两个问题:

  1. 延迟生成半成品Bean:在三级缓存架构下,首先生成的是对象工厂放入三级缓存,当后续存在循环依赖时才会生成半成品Bean放入二级缓存并被使用。
  2. 支持动态增强(代理),能生成原始对象或代理对象。在Bean的创建过程中,spring会检测该类是否使用了切面或增强(事物注解),如果是则证明需要代理对象,那么在使用objectFactory.getObject()时就会生成代理对象,否则生成原始对象。
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在被使用时才注入。

@Component
public class BeanA {@Autowired@Lazyprivate BeanB beanB; // 延迟注入// 其他方法...
}@Component
public class BeanB {@Autowiredprivate BeanA beanA; // 直接注入// 其他方法...
}

http://www.ppmy.cn/devtools/158493.html

相关文章

谈谈云计算、DeepSeek和哪吒

我不会硬蹭热点,去分析自己不擅长的跨专业内容,本文谈DeepSeek和哪吒,都是以这两个热点为引子,最终仍然在分析的云计算。 这只是个散文随笔,没有严谨的上下游关联关系,想到哪里就写到哪里。 “人心中的成见…

【JavaEE进阶】依赖注入 DI详解

目录 🌴什么是依赖注入 🎄依赖注入的三种方法 🚩属性注⼊(Field Injection) 🚩Setter注入 🚩构造方法注入 🚩三种注⼊的优缺点 🌳Autowired存在的问题 🌲解决Autowired存在的…

[作业]数池塘

正文&#xff1a; #include <iostream> #include <iomanip> using namespace std; struct Point{int x,y,v,lx,ly;Point(){};Point(int a,int b,int c ,int d,int e){xa;yb;vc;lxd;lye;} }; int dx[4]{0,1,0,-1}; int dy[4]{1,0,-1,0}; char map[1000][1000]; int …

AI大模型零基础学习(4):私有化部署与企业级应用——打造你的专属智能大脑

从“公共API调用”到“自主可控”的跨越式升级 一、为什么企业需要私有化大模型&#xff1f; 1.1 三大核心诉求 数据安全&#xff1a;防止敏感商业数据&#xff08;客户信息/财务报告/研发文档&#xff09;外流 合规要求&#xff1a;满足GDPR、等保三级等数据本地化存储规范 …

Transformer解码器终极指南:从Masked Attention到Cross-Attention的PyTorch逐行实现

Transformer 解码器深度解读 代码实战 1. 解码器核心作用 Transformer 解码器的核心任务是基于编码器的语义表示逐步生成目标序列&#xff08;如翻译结果、文本续写&#xff09;。它通过 掩码自注意力 和 编码器-解码器交叉注意力&#xff0c;实现自回归生成并融合源序列信息…

GAIA介绍

项目地址&#xff1a;https://microsoft.github.io/GAIA/ 论文地址&#xff1a;https://arxiv.org/pdf/2311.15230.pdf GAIA&#xff08;Generative AI for Avatar&#xff09;是由微软团队提出的一种零样本说话头像生成框架&#xff0c;旨在通过输入语音和单张肖像图像生成自…

【天梯赛】L2-001紧急救援(用迪杰斯特拉找出权重和最小的最短路径)

解题反思 尝试DFS&#xff1a;开始使用DFS来遍历求解&#xff0c;但 DFS 存在大量重复计算&#xff0c;像同一节点会被多次访问并重复计算路径信息&#xff0c;导致时间复杂度高&#xff0c;部分测试点未通过 改用迪杰斯特拉&#xff1a;为了求解&#xff0c;设置了很多的辅助…

【Cocos TypeScript 零基础 15.1】

目录 见缝插针UI脚本针脚本球脚本心得_旋转心得_更改父节点心得_缓动动画成品展示图 见缝插针 本人只是看了老师的大纲,中途不明白不会的时候再去看的视频 所以代码可能与老师代码有出入 SIKI_学院_点击跳转 UI脚本 import { _decorator, Camera, color, Component, directo…