Spring Boot扫描bean之ClassPathBeanDefinitionScanner

2024-03-11 23:08

本文主要是介绍Spring Boot扫描bean之ClassPathBeanDefinitionScanner,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文源码基于Spring Boot 2.2.8
本文不赘述上下文信息,需要大家自己确定这个过程在Spring Boot启动流程中地位,否则本文的意义将大打折扣。

从Spring 2.5开始,用户就可以通过编程的方式注册bean,而不用在xml中通过复杂的方式配置bean,这对当时Spring使用者来说是一个天大的惊喜,终于可以告别动辄几千行甚至几万行的xml配置文件了。而这一功能的核心是在指定路径扫描带指定注解的bean,并根据自动装配注解完成属性注入。这样用户就只用提供一个basePackage路径,然后在代码上写上适当的注解就完成所有bean的注入了。

ClassPathBeanDefinitionScanner就是一个具备扫描指定路径,并注入对应BeanDefinition能力的工具类。官方的解释为:
org.springframework.context.annotation.ClassPathBeanDefinitionScanner
始自Spring 2.5
这个类提供的时候,Spring的xml中已经可以不用配置业务bean了,完全可以通过配置<context:component-scan base-package="com.x.y"></context:component-scan>来达到自动扫描,自动DI的效果。
官方对这个类的说明是:
ClassPathBeanDefinitionScanner是一个BefanDefinition扫描器,它检测classpath上的bean候选者,使用注册器(BeanFactory或ApplicationContext)注册相应的BeanDefiniton。通过可配置的类型过滤器检测候选类。 默认的类型过滤器含有使用Spring的@Component,@Repository,@Service或@Controller注解的类。还支持Java EE 6的javax.annotation.ManagedBean和JSR-330的javax.inject.Named注解(如果可用)。

一般而言,我们应用的环境是Servlet 的WebServer,我们还需要使用注解配置,最后ApplicationContext的具体类型会是:
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext
他的无参构造方法实例化了2个类型作为自己的成员变量,还是需要知道这2个类型在实例化时有那些逻辑。
AnnotationConfigServletWebServerApplicationContext是ApplicationContext继承体系中最底层的,也就是说这个ClassPathBeanDefinitionScanner只有他自己会使用,他的父类是没有办法使用到的。
另外这里的postProcessBeanFactory()方法refresh容器中的一个流程,即refresh()之postProcessBeanFactory()流程。

public AnnotationConfigServletWebServerApplicationContext() {this.reader = new AnnotatedBeanDefinitionReader(this);this.scanner = new ClassPathBeanDefinitionScanner(this);@Overrideprotected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {super.postProcessBeanFactory(beanFactory);if (this.basePackages != null && this.basePackages.length > 0) {this.scanner.scan(this.basePackages);}if (!this.annotatedClasses.isEmpty()) {this.reader.register(ClassUtils.toClassArray(this.annotatedClasses));}}
}

 org.springframework.context.annotation.ClassPathBeanDefinitionScanner (since 2.5),早期通过扫描路径+@Component(since 2.5)实现bean的注册
org.springframework.context.annotation.AnnotatedBeanDefinitionReader (since 3.0),通过手动方式实现bean的注册
本文只讲解ClassPathBeanDefinitionScanner这个类型,有关AnnotatedBeanDefinitionReader的讲解请点击这篇文章《Spring Boot扫描bean之AnnotatedBeanDefinitionReader》
ClassPathBeanDefinitionScanner是一个工具类,只要传递相应的参数实例化了即可使用其扫描bean的能力。
可以看到,ClassPathBeanDefinitionScanner构造函数中最多为4个参,其中重要的有3个,registry是BeanDefinition存储的地方,environment是环境,resourceLoader是类路径相关信息。
其实AnnotationConfigServletWebServerApplicationContext中的这个scanner没有多大意义,因为后续真正扫描bean的是重新new出来的一个scanner在工作。我们通过断点可以知道,这里的org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext#postProcessBeanFactory()方法中基本是没有条件调用到scan()的,所以真正扫描bean的是流程是在:
refresh()之invokeBeanFactoryPostProcessors()。
org.springframework.context.annotation.ClassPathBeanDefinitionScanner

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {this(registry, true);}public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {this(registry, useDefaultFilters, getOrCreateEnvironment(registry));}public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,Environment environment) {this(registry, useDefaultFilters, environment,(registry instanceof ResourceLoader ? (ResourceLoader) registry : null));}public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,Environment environment, @Nullable ResourceLoader resourceLoader) {Assert.notNull(registry, "BeanDefinitionRegistry must not be null");this.registry = registry;if (useDefaultFilters) {registerDefaultFilters();}setEnvironment(environment);setResourceLoader(resourceLoader);}}

下面我们看看几个注解的时间
@Autowire(since 2.0)
@Repository(since 2.0)
@Component(since 2.5)
@Service(since 2.5)

@Controller(since 2.5)
@Scope(since 2.5)
@ComponentScan(since 3.1) 意味全注解开发的时代来临

可以看到ClassPathBeanDefinitionScanner早于AnnotatedBeanDefinitionReader诞生,也就是说,Spring 2.5 时期,没有AnnotatedBeanDefinitionReader也是完全可以提供给用户使用的。
那时的Spring 可以使用依赖注入功能,但是bean还是要在xml中配置,只不过在xml中配置的bean不用配置依赖了,这个已经减少了一大半以上的xml内容了,大大提高了编程效率。

三、ClassPathBeanDefinitionScanner源码解析

这个类是个非常重要的类,其提供的方法是可以被第三方组件调用的,所以需要大家注意
其中scan()方法可以通过对象调用,doScan()方法需要继承后调用,mybatis-spring组件就调用了这2个方法,用来扫描Mapper接口,并生成代理对象。
真正工作的方法是:
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan()

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {/*** Perform a scan within the specified base packages.* @param basePackages the packages to check for annotated classes* @return number of beans registered*/public int scan(String... basePackages) {int beanCountAtScanStart = this.registry.getBeanDefinitionCount();doScan(basePackages);// Register annotation config processors, if necessary.if (this.includeAnnotationConfig) {AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);}return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);}/*** Perform a scan within the specified base packages,* returning the registered bean definitions.* <p>This method does <i>not</i> register an annotation config processor* but rather leaves this up to the caller.* @param basePackages the packages to check for annotated classes* @return set of beans registered if any for tooling registration purposes (never {@code null})*/protected Set<BeanDefinitionHolder> doScan(String... basePackages) {Assert.notEmpty(basePackages, "At least one base package must be specified");Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();for (String basePackage : basePackages) {Set<BeanDefinition> candidates = findCandidateComponents(basePackage);for (BeanDefinition candidate : candidates) {ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);candidate.setScope(scopeMetadata.getScopeName());String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);if (candidate instanceof AbstractBeanDefinition) {postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);}if (candidate instanceof AnnotatedBeanDefinition) {AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);}if (checkCandidate(beanName, candidate)) {BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);definitionHolder =AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);beanDefinitions.add(definitionHolder);registerBeanDefinition(definitionHolder, this.registry);}}}return beanDefinitions;}}

Spring内部调用doScan()的地方有2处:分别是
refresh()之postProcessBeanFactory()
refresh()之invokeBeanFactoryPostProcessors()
前者因为条件不满足,无法调用到doScan()内部去,后者能够调用到,并扫描出了所有bean。
第一处使用的地方比较简单,所以不做过多说明
第一处:

org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext#postProcessBeanFactory()

@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {super.postProcessBeanFactory(beanFactory);if (this.basePackages != null && this.basePackages.length > 0) {this.scanner.scan(this.basePackages);}if (!this.annotatedClasses.isEmpty()) {this.reader.register(ClassUtils.toClassArray(this.annotatedClasses));}
}

第二处:是在解析@ComponentScan注解时调用
也就是说doScan()被调用多少次取决于@ComponentScan注解使用了多少次。
对于这个调用,我们需要学到的只是点时Spring实在refresh()方法的

上面调用链路不太适合阅读,整个调用链路先后涉及的类是:
org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors()
org.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors()
org.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanDefinitionRegistryPostProcessors()
org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry()
org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions()
org.springframework.context.annotation.ConfigurationClassParser#parse()
org.springframework.context.annotation.ConfigurationClassParser#parse()
org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass()
org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass()
org.springframework.context.annotation.ComponentScanAnnotationParser#parse()
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan()

这篇关于Spring Boot扫描bean之ClassPathBeanDefinitionScanner的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/799364

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2