持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
基于 Spring Framework v5.2.6.RELEASE
概述
上一篇从 AnnotationConfigApplicationContext 的构造方法入手,分析了上下文初始化的第一部分,也就是上下文对象中一些成员变量的初始化,和一些相关的准备工作。本文重点分析构造方法中的第二步,AnnotationConfigApplicationContext 中的scan方法,它主要完成组件的扫描及其他相关的工作。
基于包路径的组件扫描
直接进入方法的源码。
// org.springframework.context.annotation.AnnotationConfigApplicationContext#scan
@Override
public void scan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
this.scanner.scan(basePackages);
}
这里比较简单,确保调用方法传入的basePackages不为空,然后将扫描的任务交给scanner的同名方法。这个scanner就是在之前的步骤中初始化好的 ClassPathBeanDefinitionScanner 成员变量。
进入scanner的scan方法。
// org.springframework.context.annotation.ClassPathBeanDefinitionScanner#scan
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);
}
这个方法的逻辑比较清晰。首尾两行代码是为了计算在这个方法执行期间注册的 BeanDefinition 的数量,并作为结果返回。中间的代码主要包含两个步骤。
首先调用了doScan方法,在 Spring 的编码习惯中,以do作为方法名前缀的方法,通常用来执行一个工作的核心逻辑。
然后,紧跟着的if判断条件中的成员变量默认值为true,因此if语句块中的方法调用会被执行。不过,读过上一篇的话,应该很熟悉这行代码。在初始化 AnnotatedBeanDefinitionReader 的结尾处,这个方法已经被执行过一遍了,其中主要做的事情是向容器中注册了一些 BeanDefinition,因为注册前都进行了判断,所以,这里在执行一次并不会对之前的注册有影响。
因此,接下来分析的重点就是doScan方法。
// org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
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;
}
先整体分析一下方法中的流程。
方法的开头,定义了一个beanDefinitions集合用来存放 BeanDefinitionHolder 对象,这个集合最终会作为结果返回,不过之前的代码调用中并没有用到这个返回值。
接下来,首先会对basePackages中所有的包路径进行遍历,通过findCandidateComponents方法,从每个包路径中获取到一个 BeanDefinition 集合,并遍历这个集合,分别对每一个 BeanDefinition 执行后续的步骤。这里的findCandidateComponents方法,从名称可以推测出,它的作用是在给定的包路径下进行扫描,找到候选的组件,然后封装成 BeanDefinition 并返回集合。
然后,对查找到的 BeanDefinition 进行处理,比如对特定类型的 BeanDefinition 进行后处理等。
最后,通过registerBeanDefinition方法,将这些 BeanDefinition 注册到容器中。
可以看出,doScan方法并非只是扫描这么简单,它还会将扫描到的注解配置封装成 BeanDefinition,并注册到容器中。下面将这个方法分成三部分来详细分析。
BeanDefinition 加载
首先是从给定的包路径获取 BeanDefinition 的过程,这一步是通过findCandidateComponents完成的,我们进入方法查看源码。
// org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
这里的this.componentsIndex默认是空的,所以,我们进入scanCandidateComponents(basePackage)方法。
// org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
for (Resource resource : resources) {
if (resource.isReadable()) {
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
candidates.add(sbd);
}
else {
}
}
else {
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex);
}
}
else {
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
return candidates;
}
这个方法代码比较多,因此我在这里删减了记录日志的部分,不影响分析代码的原理。
第一步,是将参数中的basePackage进行处理,它可以将一个类似于com.pseudocode的包路径,处理成classpath*:com/pesudocode/**/*.class,如果其中包含占位符,也会用相应的参数值替换。然后使用处理后的路径,获得一个resources资源数组。
接下来,对这个资源数组进行遍历。在对资源的处理中,首先会创建一个资源对应的 MetadataReader 对象,用来获取资源的元信息。然后通过isCandidateComponent方法,来筛选符合条件的资源。
我们进入这个方法查看筛选的逻辑。
// org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent(org.springframework.core.type.classreading.MetadataReader)
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
这里逻辑就非常清晰了,还记得上一篇分析中,初始化 ClassPathBeanDefinitionScanner 时,调用了一个registerDefaultFilters方法吗?其中,向includeFilters集合中添加了几个注解类型的 TypeFilter。这个方法的作用就是根据给定的资源元信息,是不是能够被includeFilters中的 TypeFilter 筛选到,简而言之,就是判断一个类型是不是包含了指定的注解。
再回到scanCandidateComponents方法。
如果当前遍历的资源(类)包含了指定的注解,那么他会被封装成一个 ScannedGenericBeanDefinition 类型的 BeanDefinition。这里可以顺便了解一下 ScannedGenericBeanDefinition,先看它的类关系图。
图中可以看到,它是 GenericBeanDefinition 的子类,GenericBeanDefinition 是 Spring 从 XML 配置文件加载 BeanDefinition 是用到的类型。其次,它还实现了 AnnotatedBeanDefinition 接口,提供了获取 BeanDefinition 注解元信息的能力。
了解完 ScannedGenericBeanDefinition,再次回到scanCandidateComponents方法中。
得到 BeanDeffinition 后,会再次通过一个isCandidateComponent方法,对 BeanDeffinition 进行验证,验证通过后的 BeanDeffinition 会被添加到isCandidateComponent方法最终返回的结果集中。
最后我们再看一下这个isCandidateComponent方法。
// org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent(org.springframework.beans.factory.annotation.AnnotatedBeanDefinition)
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
return (metadata.isIndependent() && (metadata.isConcrete() ||
(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
}
这里的验证条件比较好理解,主要是确保了当前的 BeanDeffinition 对应的类型,是可以被实例化的。
总结
本文介绍了 Spring 基于注解初始化上下文的关键步骤doScan方法的原理的第一部分,即从给定的包路径中查找符合条件的类并封装成对应的 BeanDefinition。后续的步骤将在下一篇文章中分析。