【小白的Spring源码手册】 Bean的扫描、装配和注册,面试学习可用

news2024/11/28 6:49:36

目录

  • 前言
  • 源码学习
    • Bean配置
      • 1. 注解
      • 2. xml配置
    • Bean扫描、装配、注册
      • 1. 扫描
      • 2. 装配BeanDefinition
      • 3. 校验BeanDefinition
      • 4. 注册BeanDefinition
  • 总结


前言

如今Spring框架功能众多,每次打开Spring源码,要么就是自顶向下从整个框架来了解Spring整体流程,然后一不小心就陷入了细节无法自拔,要么就是从某个核心功能点看起,然后过了几分钟就陷入茫然。总之过程是十分的痛苦,结果却不尽人意。

这一次,我打算从最熟悉的功能模块IoC开始学习。IoC容器在Spring中扮演着重要作用,它控制了所有bean对象的整个生命周期。看源码时,接触到的第一个接口BeanFactory就是IoC容器的顶层接口,能控制对象生命周期;而ApplicationContext对前者进行了扩展,拥有事件发布、国际化信息支持等新特性。在这里,我打算先忽略IoC的顶层流程,根据Bean生成的过程,学习Bean在Spring中是如何被扫描、加载、生成、最后实例化。最后再将Bean的整个声明周期与IoC容器串起来。

通过扫描注解类的Bean实例化流程参考下图:
在这里插入图片描述

下面的源码解析中,省略了很多不必要的类、方法和代码块,将核心代码提了出来,以方便大家忽略非主流程的细节更专注于我们的核心流程。整个流程我已经提取出了可执行的核心代码,大家可以debug熟悉一下定义Bean的流程,地址见文末连接。





源码学习

Bean配置

spring要对bean进行管理,需要扫描相应配置,完成后进行统一管理,spring(5.0+)目前比较流程扫描方式为注解xml,相关配置如下:

1. 注解

配置包:

// spring
@ComponentScan(value = {"com.example.springreading",})
// spring boot,默认对启动类同包下的所有包进行扫描
@SpringBootApplication(scanBasePackages = {"com.example.springreading",})

在spring中,应用上下文会扫描被@Component @Repository @Controller @Service等被Component注解的类。只有被这些注解标注的类,才会被IoC容器管理。

AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext("com.example.springreading");

AnnotationConfigApplicationContext类中有两个关键字段:

  • AnnotatedBeanDefinitionReader reader:用于扫描指定class类(Class<?>… componentClasses)文件。
  • ClassPathBeanDefinitionScanner scanner:用于扫描指定包(String… basePackages)中文件。

它们两者的功能都是一样的,就是扫描类文件,并将Bean定义注册到内部的IoC容器。

// AnnotationConfigApplicationContext字段
private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;
// AnnotationConfigApplicationContext父类GenericApplicationContext字段
private final DefaultListableBeanFactory beanFactory; // new DefaultListableBeanFactory()

注意:AnnotationConfigApplicationContext父类中是有维护一个IoC容器的,应用上下文的Bean操作都是通过该容器实现的。

通过类名初始化,AnnotationConfigApplicationContext类会通过reader来进行类扫描和注册,通过包名初始化,则会通过scanner

// reader
public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
	this();
	register(componentClasses);
	refresh();
}
// scanner
public AnnotationConfigApplicationContext(String... basePackages) {
	this();
	scan(basePackages);
	refresh();
}
// 无参构造
public AnnotationConfigApplicationContext() {
	StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create");
	this.reader = new AnnotatedBeanDefinitionReader(this);
	createAnnotatedBeanDefReader.end();
	this.scanner = new ClassPathBeanDefinitionScanner(this);
}



2. xml配置

配置包:

<!-- spring.xml配置文件 -->
<!-- 注解依赖扫描 加载@Component等注解标注的bean 这样不用在下面添加bean标签来手动生成bean实例-->
<context:component-scan base-package="com.example.springreading"/>

xml配置文件扫描

ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("/xml/bean.xml");
// XmlBeanDefinitionReader是其内部的bean定义扫描类
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(registry);

精力有限,本文仅学习注解有关类,xml原理是类似的,大家可以根据兴趣自行学习。





Bean扫描、装配、注册

这里选择通过包名初始化应用上下文,扫描、装配、注册等一系列操作均由ClassPathBeanDefinitionScanner scanner来实现的。

String[] basePackages = new String[]{"com.example.springreading"};
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(basePackages);



1. 扫描

scanner的扫描入口

int scan = scanner.scan(basePackages);
// 由doScan执行具体的扫描操作
doScan(basePackages);

遍历所有包,scanner会遍历所有包,依次加载包下面的所有class类

for (String basePackage : basePackages) {
	Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
	// ……
}	

findCandidateComponents方法中,会将扫描到的class文件读取为Resource资源,然后将Resource资源转为元数据读取器MetadataReader,最后通过包装元数据的方式创建BeanDefinition

源码如下:

A. Resource类载入Java class文件:

String packageSearchPath = "classpath*:" + "com/example/springreading" + '/' + "**/*.class";
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);

B. 生成元数据读取器(MetadataReader)

MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory();
// 遍历resources
MetadataReader metadataReader = metadataReaderFactory .getMetadataReader(resource);

在这里插入图片描述

C. 生成BeanDefinition:

ScannedGenericBeanDefinition beanDefinition= new ScannedGenericBeanDefinition(metadataReader);

到目前为止一个bean定义就被创建了,但是Bean的初始化还没完,我们还有很多bean相关的内容需要完善。



2. 装配BeanDefinition

BeanDefinition接口或实现类说明:

BeanDefinition:是Bean定义的顶层接口类。
AnnotatedBeanDefinition:该接口是BeanDefinition子类接口,用于承载注解内容。
ScannedGenericBeanDefinition:继承GenericBeanDefinition类,是AnnotatedBeanDefinition的实现之一。

在装配BeanDefinition这块,涉及到很多业务经常使用的注解,@Component、@Scope、@Lazy、@Primary、@DependsOn等注解都会在这个阶段被解析并装配进BeanDefinition。这些注解的解析和装配的过程类似。

源码参考:

A. @Scope(value = "singleton", proxyMode = ScopedProxyMode.DEFAULT)
设置作用域,这个bean作用范围是singleton还是prototype

ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();
// 作用域元数据
ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(beanDefinition);
// 设置作用域
beanDefinition.setScope(scopeMetadata.getScopeName());

作用域这个注解,是通过Spring的一套公用的注解提取工具提取的,原理是通过当前AnnotatedBeanDefinition元数据读取器 读取元数据,以得到注解类,最后根据注解名字得到指定的注解以及注解的字段内容。

在这里插入图片描述

ScopeMetadataResolver#resolveScopeMetadata内部实现如下:

ScopeMetadata metadata = new ScopeMetadata();
AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) beanDefinition;
// 获取元数据中的注解对象
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(annDef.getMetadata(), Scope.class);
// 获取注解属性值并设置到作用域类中
metadata.setScopeName(attributes.getString("value"));
metadata.setScopedProxyMode(attributes.getEnum("proxyMode"));

B. @Component(value = "businessService")

设置bean的实例名称:beanName,通过读取注解的value来获取bean名,大小写敏感。

除了Component,还有很多注解可以实现beanName设置。譬如大家常见的@Controller、@Service、@Repository等等。

// 单例模式的类名生成器
BeanNameGenerator beanNameGenerator = AnnotationBeanNameGenerator.INSTANCE;
String beanName = beanNameGenerator.generateBeanName(beanDefinition, context);

要实现通过注解设置BeanName,只需要实现如下几个条件之一:

  • 使用@Component注解,设置value值
  • 使用@Component注解注解的注解,类似@Service,拥有value属性并设值
  • 使用@ManagedBean或@Named(年代有些久远就不提了),有value属性并设值
boolean isStereotype = annotationType.equals("org.springframework.stereotype.Component") ||
				metaAnnotationTypes.contains("org.springframework.stereotype.Component") ||
				annotationType.equals("javax.annotation.ManagedBean") ||
				annotationType.equals("javax.inject.Named");
// 注解的属性值,要有string类型的value属性,且属性值不能为空
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type);

当上面的条件不满足时,beanName默认使用bean定义的类名,首字母小写

// 为空时的处理
String decapitalize = Introspector.decapitalize(ClassUtils.getShortName(Objects.requireNonNull(beanDefinition.getBeanClassName())));

C. 其它注解
@Lazy(value = false)
@DependsOn
@Primary
@Description(value = "Description of bean definition")
通过AnnotationConfigUtils工具来加载bean的一些通用bean注解,流程跟Scope注解是相同的。

AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDefinition);
// 解析
AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
// ……



3. 校验BeanDefinition

在BeanDefinition装配完成后,需要通过AnnotationConfigApplicationContext context来校验是否存在beanName

// DefaultListableBeanFactory#containsBeanDefinition
boolean containsBeanDefinition = context.containsBeanDefinition(beanName);

在上下文校验代码中,由DefaultListableBeanFactory beanFactoryIoC容器内的Map<String, BeanDefinition> beanDefinitionMap完成最终校验(BeanDefinition最终都会被注册到beanDefinitionMap中)。

context.getBeanFactory().containsBeanDefinition(beanName);
// IoC容器中保存初始Bean定义的缓存
// Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
// this.beanDefinitionMap.containsKey(beanName)



4. 注册BeanDefinition

现在Bean的扫描、加载、定义、装配乃至校验都做完了,可以完成注册了。

// 使用BeanDefinitionHolder来包装,如果有别名的话,这里会添加Bean的别名名称,但是通过注解类扫描的Bean,没有别名
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);

注册和校验相同,均由DefaultListableBeanFactory beanFactoryIoC容器完成,beanFactory会将该BeanDefinition保存至beanDefinitionMap中。

// 注册方法
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, context);
// AnnotationConfigApplicationContext#registerBeanDefinition
context.registerBeanDefinition(beanName, beanDefinition);
// DefaultListableBeanFactory#registerBeanDefinition
beanFactory.registerBeanDefinition(beanName, beanDefinition);
// 最后添加至缓存中
this.beanDefinitionMap.put(beanName, beanDefinition);





总结

有关Bean的扫描、装配、校验到包装到此就结束了,本文的重点虽然不在IoC容器,但是我们在进行Bean定义的过程中,我们仍然会接触到它,以小见大,从一些基础的功能开始慢慢了解整个Spring框架,我觉得这是一个很好的立足点。

接下来,我会从IoC容器实例化Bean的流程来进一步了解整个Spring IoC机制。

DEMO代码:https://github.com/VsLegend/spring-reading.git

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1168804.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YOLOv5:按每个类别的不同置信度阈值输出预测框

YOLOv5&#xff1a;按每个类别的不同置信度阈值输出预测框 前言前提条件相关介绍YOLOv5&#xff1a;按每个类别的不同置信度阈值输出预测框预测修改detect.py输出结果 验证修改val.py输出结果 参考 前言 由于本人水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评改正。更…

关于FastJSON序列化Bean时对get方法调用的细节

结论 使用JSON.toJSONString去序列化Bean的时候 FastJSON会把Bean里面的get开头&#xff0c;有返回值且没有参数的方法都调用一遍。 看代码 package org.example.domain;import lombok.Getter; import lombok.Setter;/*** program: parent_pro* description:* author: 渭水* c…

为何袁世凯要把“元宵节”改为“上元节”?

网民把春节除夕日排除在法定假期之外的相关热议&#xff0c;在微博评论区部分已被关闭。官方学者的解释是&#xff1a;“回归传统。” 这就令人难免要回顾历史&#xff0c;并发觉只有在袁世凯称帝之后&#xff0c;才有过取消“元宵节”改为“上元节”的笑话&#xff0c;因为“元…

数模国赛——多波束测线问题模型建立研究分析

第一次参加数模国赛&#xff0c;太菜了~~~~意难平 问题一 画出与测线方向垂直的平面和海底坡面的交线构成一条与水平面夹角为&#x1d400;的斜线的情况下的示意图进行分析&#xff0c;将覆盖宽度分为左覆盖宽度和右覆盖宽度&#xff0c;求出它们与海水深度和&#x1d400;、…

纷享销客荣获最佳制造业数字营销服务商奖

2023年10月26日&#xff0c;第二届中国制造业数智化发展大会在上海盛大召开。本次大会汇聚了制造行业的顶尖企业和专家&#xff0c;共同探讨如何通过数字化转型赋能企业自身成长&#xff0c;实现信息化向数字化的升级转型。 在本次盛会上&#xff0c;纷享销客以其卓越的基本面、…

[激光原理与应用-75]:西门子PLC系列选型

目录 一、西门子PLC PLC系列 二、西门子PLC S7 1200系列 2.1 概述 2.2 12xx系列比较 三、西门子 PLC 1212C系列 四、主要类别比较 4.1 AC/DC/RLY的含义 4.2 AC/DC/RLY与DC/DC/DC 4.3 直流输入与交流输入比较 4.4 继电器输出与DC输出的区别 一、西门子PLC PLC系列 …

人工智能与卫星:颠覆性技术融合开启太空新时代

人工智能与卫星&#xff1a;颠覆性技术融合开启太空新时代 摘要&#xff1a;本文将探讨人工智能与卫星技术的融合&#xff0c;并介绍其应用、发展和挑战。通过深入了解这一领域的前沿动态&#xff0c;我们将展望一个由智能卫星驱动的未来太空时代。 一、引言 近年来&#xf…

【华为OD题库-018】AI面板识别-Java

题目 Al识别到面板上有N(1<N≤100)个指示灯&#xff0c;灯大小一样&#xff0c;任意两个之间无重叠。由于AI识别误差&#xff0c;每次识别到的指示灯位置可能有差异&#xff0c;以4个坐标值描述Al识别的指示灯的大小和位置(左上角x1,y1&#xff0c;右下角x2.y2)。请输出先行…

可视化协作软件有哪些?这10款神器助力团队合作!

可视化协作已经成为一个时下热门词汇&#xff0c;问题是对其并没有一个清晰的定义。有人认为它代表了一个云端环境&#xff0c;具有能够使办公室、混合办公和远程员工一起工作的功能。其他人则认为可视化协作不过是数字化白板而已。 随着这个术语变得更加流行&#xff0c;许多…

番外---9.0 firewall 网络

### 网络配制方式&#xff1a; 00&#xff1a;依据图形界面形式配置&#xff08;nmtui&#xff09;&#xff1b; 01&#xff1a;命令形式配置(nmcli)&#xff1b; 02&#xff1a;使用系统菜单配置&#xff1b; 00&#xff1a;依据图形界面形式配置&#xff08;nmtui&#xff0…

【Redis】进阶篇--用JAVA代码操作Redis

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Redis的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Java连接Redis 1.导入pom依赖 ​2.Ja…

Spring Boot中解决跨域问题(CORS)

1. 跨域介绍 首先解释什么是跨域&#xff0c;跨域就是前端和后端的端口号不同&#xff1b;会产生跨域问题&#xff0c;这里浏览器的保护机制&#xff08;同源策略&#xff09;。 同源策略&#xff1a;前端和后端的协议、域名、端口号三者都相同叫做同源。 我们看一下不同源&am…

SAP SD 定价 删除不满足条件的的条件类型

项目上的需求&#xff1a;当销售订单行项目类别满足条件时&#xff0c;根据配置表&#xff0c;删除不满足条件的的条件类型。 直接上增强点&#xff0c;bapi也能跑到这个位置。

优思学院|领导力是什么?与管理能力有何不同?领导力也能够后天培养!

什么是领导力&#xff1f; 在现代商业世界中&#xff0c;领导力不再仅仅是指挥下属&#xff0c;而是一种复杂的技能&#xff0c;需要具备多方面的能力和特质。领导力是指一个人能够影响、激励和指导他人&#xff0c;以达成共同目标的能力。与管理能力相比&#xff0c;领导力更…

JavaScript:事件循环机制(EventLoop)

一、理解进程、线程 进程是操作系统中的基本概念之一&#xff0c;指的是一个正在运行中的程序&#xff0c;包括了程序的执行代码、数据、资源等。操作系统为每个进程分配一定的系统资源&#xff0c;例如内存空间、文件和设备等&#xff0c;以便进程能够正常运行。 线程是进程…

【jvm】虚拟机栈之方法返回地址

目录 一、说明二、方法退出三、代码示例四、截图示例 一、说明 1. 存放调用该方法的pc寄存器的值。 2. 一个方法的结束&#xff0c;有两种方式&#xff1a;正常执行完成和出现未处理的异常的非正常退出 3. 无论通过哪种方式退出&#xff0c;在方法退出后都返回到该方法被调用的…

【lvgl】linux开发板搭建环境

前言 本章介绍如何在linux开发板准备好了fb0的情况下移植lvgl。 抓取源码 git clone https://github.com/lvgl/lvgl.git git clone https://github.com/lvgl/lv_drivers.git git clone https://github.com/lvgl/lv_demos.git git clone https://github.com/lvgl/lv_port_lin…

【Docker】手把手教你使用Docker搭建kafka【详细教程】

目录 前提条件 1.安装Zookeeper 1.1运行ZooKeeper容器 2.运行Kafka容器 2.1启动Kafka容器 3.验证 3.1进入Kafka容器 3.2查看容器状态 3.3查看容器日志 3.4重新启动容器 3.5创建测试主题 前提条件 1. 安装Docker: 确保你已经在你的Windows机器上安装了Docker。你可以…

升级智能监控,真香!

随着社会的发展与进步&#xff0c;传统依赖看的监控已经无法满足大众的需求&#xff0c;不够智能、识别不精准&#xff0c;传统监控的弊端也日益显现&#xff0c;智能监控升级迫在眉睫。 升级智能监控&#xff0c;不仅能够促进公共安全&#xff0c;同时也能促进社会文明的发展…

Vue3多页面开发实践

前言&#xff1a; 项目需求&#xff0c;把项目中的一个路由页面单摘出来作为一个新的项目。项目部署到服务器上后&#xff0c;通过一个链接的形式可以直接访问到新项目的页面。 解决方式&#xff1a; 使用Vue多页面方式打包项目 实现步骤&#xff1a; 1、在项目的src目录下&am…