【spring专题】spring如何解析配置类和扫描包路径

文章目录

目标

这是我们使用注解方式启动spring容器的核心代码

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
User user = (User) applicationContext.getBean("user");
user.printName();

其中配置类MyConfig的代码是

@ComponentScan(value = "com.mydemo")
public class MyConfig {
}

现在我们的目标是搞清楚spring是怎么解析这个配置类并且扫描该配置类包路径下的bean?

重要的组件

  • AnnotatedBeanDefinitionReader : spring容器启动的时候就会创建这个读取器,主要是将类以BeanDefinition的方式保存到bean工厂(DefaultListableBeanFactory)
    在创建这个读取器的时候,spring会默认添加一个ConfigurationClassPostProcessor的BeanDefinition,这个就是在解析配置类时的主要对象,在AnnotationConfigUtils类的registerAnnotationConfigProcessors中实现

    if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
    }

  • ClassPathBeanDefinitionScanner : 路径扫描器,在spring启动的时候就会创建,主要功能就是对类路径进行扫描,内含一些扫描规则,例如在创建时候就会内置一个Component注解的过滤器

    protected void registerDefaultFilters() {
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    ...
    }

加载配置类

我们的配置类是由AnnotatedBeanDefinitionReader类的doRegisterBean方法,转成BeanDefinition存到bean工厂的beanDefinitionMap中,基于ASM获取一个类信息转成BeanDefinition。

转成的核心代码

AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);

得到配置类对象的AnnotatedGenericBeanDefinition后,虽然还没有加载类,但是已经获取到了类的注解信息。

虽然都是带有BeanDefinition,但是保存到bean工厂的BeanDefinition和这个是不一样的,这个AnnotatedGenericBeanDefinition主要是一些注解信息,并没有类似于BeanDefinition的属性,如是否懒加载,作用域,是否依赖等。

解析AnnotatedGenericBeanDefinition注解信息的主要代码,主要就是读取Lazy、Primary 、DependsOn、Description设置成属性值

AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
if (lazy != null) {
	abd.setLazyInit(lazy.getBoolean("value"));
}
else if (abd.getMetadata() != metadata) {
	lazy = attributesFor(abd.getMetadata(), Lazy.class);
	if (lazy != null) {
		abd.setLazyInit(lazy.getBoolean("value"));
	}
}

if (metadata.isAnnotated(Primary.class.getName())) {
	abd.setPrimary(true);
}
AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
if (dependsOn != null) {
	abd.setDependsOn(dependsOn.getStringArray("value"));
}

AnnotationAttributes role = attributesFor(metadata, Role.class);
if (role != null) {
	abd.setRole(role.getNumber("value").intValue());
}
AnnotationAttributes description = attributesFor(metadata, Description.class);
if (description != null) {
	abd.setDescription(description.getString("value"));
}

解析AnnotatedGenericBeanDefinition后转成BeanDefinitionHolder才是我们要保存到bean工厂的BeanDefinition

BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);

如果配置类不是代理模式,就直接保存BeanDefinition到bean工厂中了,

如果是代理模式,就创建一个新的RootBeanDefinition保存到bean工厂中,主要实现的代码在ScopedProxyUtils类createScopedProxy方法中

启动解析组件

spring在启动配置类扫描的任务时,是以启动一个BeanDefinitionRegistryPostProcessor的方式调用扫描类执行的,属于一种组件化启动任务类的方式

for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
	...
	postProcessor.postProcessBeanDefinitionRegistry(registry);
	...
}

这个组件的实现类是ConfigurationClassPostProcessor,所以所有的扫描代码都在该类的postProcessBeanDefinitionRegistry方法下

定位配置类

在bean工厂的beanDefinitionMap中遍历每个元素来定位符合配置类的bd,规则校验在ConfigurationClassUtils类checkConfigurationClassCandidate方法中:

  1. 主要是确定该bd是AnnotatedBeanDefinition类型,
  2. 如果beanDef不是AnnotatedBeanDefinition的实例,则进一步检查它是否是AbstractBeanDefinition的实例并且已经有了对应的Class对象。如果是的话,接着会检查这个Class是否实现了某些特定接口(如BeanFactoryPostProcessor, BeanPostProcessor, AopInfrastructureBean, 或者EventListenerFactory)。如果确实实现了这些接口中的一个或多个,函数将返回false,表示不需要继续解析。否则,它将通过AnnotationMetadata.introspect(beanClass)方法来获取该类的注解元数据。
  3. 如果以上两种情况都不满足,代码将尝试通过MetadataReader从类路径中读取指定类名(className)的元数据。这通常涉及到加载类文件并从中提取信息。如果在这个过程中发生IO异常(例如找不到类文件),则记录错误信息并返回false。

解析配置类

解析的操作是ConfigurationClassParser来完成的,所有解析的相关逻辑都在该类的processConfigurationClass方法中,主要负责解析和注册配置类中的各种注解:

处理@PropertySource @ComponentScan @Import @ImportResour @Bean注解,这里值分析 @ComponentScan注解,因为已经获取到了类的元信息,所以就可以获取@ComponentScan配置的路径,进而进行路径扫描,扫描是交由ComponentScanAnnotationParser组件执行的,由ComponentScanAnnotationParser组件发起最终在ClassPathBeanDefinitionScanner类型的doScan来实现

扫描过程

Set<BeanDefinition> candidates = findCandidateComponents(basePackage);

通过调用findCandidateComponents方法,根据提供的基础包名(basePackage)来查找该包及其子包下的所有符合组件扫描条件的类,并将它们作为候选组件返回。每个候选组件都是一个BeanDefinition对象,表示潜在的Spring bean:

  • 构建搜索路径:
    构建一个资源模式路径,用于指示ResourcePatternResolver在哪里查找资源。这个路径包括了类路径前缀、基础包名以及资源模式(例如/**/*.class),以便于匹配所有的类文件。

    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
    resolveBasePackage(basePackage) + '/' + this.resourcePattern;

  • 获取资源

    Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);

通过getResourcePatternResolver()获取资源解析器实例,并调用其getResources方法来获取与给定模式匹配的所有资源。这里的资源是指符合路径模式的类文件。

  • 初步筛选
    遍历每个资源,使用MetadataReaderFactory为每个资源创建一个MetadataReader实例,它能够读取类的元数据而无需加载该类到JVM中。

    ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
    sbd.setSource(resource);

首先使用isCandidateComponent(metadataReader)方法初步判断资源是否可能是一个候选组件:

AnnotationMetadata metadata = beanDefinition.getMetadata();
return (metadata.isIndependent() && (metadata.isConcrete() ||
				(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
  1. 类必须是独立的(非内部类)。
  2. 同时,类必须是具体的(非接口或非抽象类),或者如果是抽象类的话,它必须包含至少一个用 @Lookup 注解标记的方法。
  • 确定是否创建为BeanDefinition

    ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
    candidate.setScope(scopeMetadata.getScopeName());
    String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);

对于每个候选的BeanDefinition,使用scopeMetadataResolver解析其作用域(scope)信息,同时为Bean生成或获取一个唯一的beanName

if (candidate instanceof AbstractBeanDefinition) {
    postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}

如果候选Bean是一个AbstractBeanDefinition类型的实例,则调用postProcessBeanDefinition方法进行额外的后处理,比如应用默认值和自动装配规则

if (candidate instanceof AnnotatedBeanDefinition) {
	AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}

如果候选Bean是AnnotatedBeanDefinition类型,那么将处理常见的注解,如@Lazy, @Primary, @DependsOn, @Role, 和 @Description等

if (checkCandidate(beanName, candidate)) {
	BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
	definitionHolder =
			AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
	beanDefinitions.add(definitionHolder);
	registerBeanDefinition(definitionHolder, this.registry);
}

检查当前候选Bean是否可以被注册到容器中,如果可以,继续执行以下操作:

创建一个BeanDefinitionHolder对象,该对象持有Bean定义、Bean名称以及其他元数据,

如果需要使用applyScopedProxyMode根据作用域代理模式来创建作用域代理,

将处理后的BeanDefinitionHolder添加到beanDefinitions列表,并注册到registry中。

在checkCandidate中还有一个方法

protected boolean isCompatible(BeanDefinition newDef, BeanDefinition existingDef) {
	return (!(existingDef instanceof ScannedGenericBeanDefinition) ||  // explicitly registered overriding bean
			(newDef.getSource() != null && newDef.getSource().equals(existingDef.getSource())) ||  // scanned same file twice
			newDef.equals(existingDef));  // scanned equivalent class twice
}

检查新的Bean定义是否与已存在的Bean定义兼容,避免重复扫描同一个文件或者类而引起的冲突。

总结

  1. 配置类加载:使用AnnotatedBeanDefinitionReader将配置类转换为BeanDefinition,并通过ASM库获取类信息。
  2. 启动解析组件:通过实现BeanDefinitionRegistryPostProcessor接口的ConfigurationClassPostProcessor组件来启动配置类的解析任务。
  3. 定位与解析配置类:遍历bean工厂中的所有BeanDefinition以定位配置类,并使用ConfigurationClassParser处理配置类上的各种注解,如@ComponentScan。
  4. 组件扫描:ClassPathBeanDefinitionScanner根据指定的基础包名查找符合组件扫描条件的类,进行初步筛选后创建BeanDefinition对象,最终注册到Spring容器中。
相关推荐
m0_748255022 分钟前
头歌答案--爬虫实战
java·前端·爬虫
小白的一叶扁舟20 分钟前
深入剖析 JVM 内存模型
java·jvm·spring boot·架构
sjsjsbbsbsn29 分钟前
基于注解实现去重表消息防止重复消费
java·spring boot·分布式·spring cloud·java-rocketmq·java-rabbitmq
苹果醋330 分钟前
golang 编程规范 - Effective Go 中文
java·运维·spring boot·mysql·nginx
沈霁晨30 分钟前
Ruby语言的Web开发
开发语言·后端·golang
DanceDonkey33 分钟前
@RabbitListener处理重试机制完成后的异常捕获
开发语言·后端·ruby
平凡的运维之路1 小时前
vsftpd虚拟用户部署
后端
chengpei1471 小时前
实现一个自己的spring-boot-starter,基于SQL生成HTTP接口
java·数据库·spring boot·sql·http
叫我:松哥1 小时前
基于Python django的音乐用户偏好分析及可视化系统设计与实现
人工智能·后端·python·mysql·数据分析·django
等一场春雨2 小时前
Java设计模式 十二 享元模式 (Flyweight Pattern)
java·设计模式·享元模式