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

编程入门 行业动态 更新时间:2024-10-17 07:32:15

【小白的Spring<a href=https://www.elefans.com/category/jswz/34/1770099.html style=源码手册】 Bean的扫描、装配和注册,面试学习可用"/>

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

目录

  • 前言
  • 源码学习
    • 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代码:.git

更多推荐

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

本文发布于:2023-11-16 12:05:29,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1622081.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:源码   手册   Spring   Bean

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!