dcddc

西米大人的博客

0%

系统学习Spring源码-SpringBoot Starter实现原理

储备知识

在理解springboot starter实现原理前,需要掌握以下内容。

SPI 机制加载三方工厂类

springboot 通过SpringFactoriesLoader加载并实例化 classpath 下META-INF/spring.factories配置文件里 KV 方式声明的工厂实现类,key=factoryClass 全路径,value 为实现类全路径。这些工厂实现类由第三方实现,并配置到META-INF/spring.factories

SpringFactoriesLoader 是 JavaSPI 机制的一种实现方式,可按一定规则加载三方的实现类。如果想自己实现一套 SPI,可以借鉴 Spring 在这里的源码

源码入口:org.springframework.core.io.support.SpringFactoriesLoader#loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader)
传入工厂类 class,方法内部加载 classpath 下META-INF/spring.factories文件,得到 Properties 对象,然后以传入的工厂类全类名为 key,获取所有三方实现的工厂类全路径 List,再反射得到三方工厂类实例 List

  • 测试代码:org.springframework.core.io.support.SpringFactoriesLoaderTests

@Import 注解方式导入 bean

@Import 导入的 bean 有这么几类:

  • 普通 bean
  • @Configuration 修饰的配置 bean
    • @Configuration 修饰的类称为配置类,配置类里定义了需要注册到 Spring 容器的 bean。同时,配置类如果使用@Import 注解,也会把 Import 导入的 bean 注册到 beanFactory
  • ImportBeanDefinitionRegistrar 注册器
  • 实现 ImportSelector 接口指定要导入的类
    • ImportSelector 一般会配合注解实现bean的动态注册。由类似@Enablexxx 注解引入@Import 注解,导入 ImportSelector 实现类,该实现类能拿到与@Enablexxx 平级的的所有注解,实现基于运行时获取的注解参数动态注册 bean。@Enablexxx 注解通常加在配置类上

@Import 一般加在配置类上,通过对配置类的解析,将@Import 导入的 bd 也注册到 bf,接下来从源码来具体分析。

ConfigurationClassPostProcessor

ConfigurationClassPostProcessor是 Spring 自带的的 bfpp,对于注解驱动的 ac,在构建 bf 阶段将其 bd 注册到 bf。在 ac 初始化流程的后置处理 bf 阶段,加载 ConfigurationClassPostProcessor,并调用它的postProcessBeanDefinitionRegistry方法,以配置类(应用启动类) bean 为源头,解析并注册其他的 bd 到 bf

通过配置类注册的 bean 主要包含这么几类:

  • ComponentScan 扫描的 bean
  • @Bean 方法定义的 bean
  • @Import 导入的 bean

ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry 的主流程是:

先从 bf 筛选出所有配置类的 bd。因为注解驱动的 ac 指定了启动的配置类,该配置类的 bd 在构建 bf 时注册,所以这里当前只会筛选出该配置类的 bd

1
2
3
4
5
6
   String[] candidateNames = registry.getBeanDefinitionNames();
...

else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
   public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
...

if (beanDef instanceof AnnotatedBeanDefinition &&
className.equals(((AnnotatedBeanDefinition) beanDef).getMetadata().getClassName())) {
// Can reuse the pre-parsed metadata from the given BeanDefinition...
metadata = ((AnnotatedBeanDefinition) beanDef).getMetadata();
}
...

// bean包含@Configuration注解,是FullConfigurationBean,bd的attr里打full标
if (isFullConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
}
// bean包含@Component、@ComponentScan、@Import、@ImportResource注解,或@Bean方式注册的bean,是LiteConfigurationBean,bd的attr里打lite标
else if (isLiteConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
}
else {
return false;
}
...

return true;
}

创建一个 ConfigurationBeanParser 对象,专门来解析配置类

1
2
3
4
5
6
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
...

parser.parse(candidates);

ConfigurationBeanParser 读取配置类 bd,创建ConfigurationClass对象来封装配置类的注解信息,然后开始真正的解析工作

  • 解析阶段还会将解析到的一些信息添加到 ConfigurationClass 对象,包括@ImportResource 指定的 bd 配置文件路径、@Import 导入的 ImportBeanDefinitionRegistrar 等
1
processConfigurationClass(new ConfigurationClass(metadata, beanName));

1、如果配置类使用了@PropertySource 注解,读取注解,将应用配置文件添加到 ac 的 environment

1
2
3
4
5
6
7
8
9
10
11
      for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}
1
2
3
   String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
Resource resource = this.resourceLoader.getResource(resolvedLocation);
addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
1
2
3
4
5
6
7
8
9
10
      MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
...

if (this.propertySourceNames.isEmpty()) {
propertySources.addLast(propertySource);
}
else {
String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
propertySources.addBefore(firstProcessed, propertySource);
}

2、获取并注册@ComponentScan 注解指定的包路径下的所有 bd,对于@Component 和@Configuration 修饰的 bean,都当做配置 bean,将其 bd 封装成 ConfigurationClass 对象,以该 bean 为源头再次解析并注册通过它引入的 bd,即这里会产生方法递归,回到步骤 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
      Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
// 扫描@ComponentScan路径下的bean,得到beanDefinition
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
// 校验bean是否为ConfigurationBean,然后对ConfigurationBean打标
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
// 如果是ConfigurationBean,再递归执行解析流程
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}

3、深度遍历配置类的所有注解,找到所有@Import 注解,将导入类封装为 SourceClass

1
2
3
4
5
6
   private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}

4、开始处理导入类

1
processImports(configClass, sourceClass, getImports(sourceClass), true);
  • 如果导入类是 ImportSelector 类型,实例化后回调它的 selectImports 方法获取导入 bean 的类路径,将这些待导入 bean 封装为 sourceClass,回到步骤四继续递归处理这些导入类
    • selectImports 方法的入参是引入@Import 注解的配置类上的所有注解信息,因此实现 selectImports 时,可以基于配置类注解信息动态选择要导入哪些 bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class < ?>candidateClass = candidate.loadClass();
// 反射实例化ImportSelector
ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
// 执行ImportSelector的自省注入
ParserStrategyUtils.invokeAwareMethods(selector, this.environment, this.resourceLoader, this.registry);
if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
this.deferredImportSelectors.add(new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
} else {
// 回调selectImports方法得到业务层想要通过Import注册的bean,传入Import所属ConfigurationBean的注解信息
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
// 加载selectImports返回的ImportClass,封装为SourceClass
Collection < SourceClass > importSourceClasses = asSourceClasses(importClassNames);
// 递归处理ImportClass
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
  • 如果导入类是 ImportBeanDefinitionRegistrar,实例化注册器 registrar,然后添加到引入该@Import 注解的配置类的 ConfigurationClass 对象
1
2
3
4
5
6
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar = (ImportBeanDefinitionRegistrar) BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
ParserStrategyUtils.invokeAwareMethods(registrar, this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
  • 如果都不是,说明导入的是普通 bean,将导入 bean 封装成 ConfigurationClass 对象,回到步骤 1 当做配置类递归解析
    • 还会为@Import 导入的 bean 缓存”导入该 Bean 的配置类上所有注解”,如果导入 bean 实现了ImportAware(Import 自省),bean 初始化前置处理时,ImportAwareBeanPostProcessor会从 ConfigurationClassParser 拿到导入 bean 所属的配置类上所有注解,通过自省接口传递给导入 bean。不过导入bean必须用@Configuration注解修饰!
    • 最佳实践:@Import 导入@Configuraion 修饰的配置 Bean,该 Bean 通过 Import 自省方式拿到引入@Import 注解的配置类上所有注解参数,在该配置 Bean 的@Bean 方式实例化 bean 时作为动态参数传入
1
2
3
4
5
6
else {
// 维护ImportClass和引入@Import的配置类注解信息,当ImportClass在bean加载的初始化前置处理阶段,自省获取引入它的配置类注解信息,see ImportAwareBeanPostProcessor
this.importStack.registerImport(currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
// 如果不是ImportSelector也不是ImportBeanDefinitionRegistrar,当做配置类重新解析
processConfigurationClass(candidate.asConfigClass(configClass));
}
1
2
3
4
5
6
7
8
9
@Override public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof ImportAware) {
ImportRegistry ir = this.beanFactory.getBean(IMPORT_REGISTRY_BEAN_NAME, ImportRegistry.class);
AnnotationMetadata importingClass = ir.getImportingClassFor(bean.getClass().getSuperclass().getName());
if (importingClass != null) { ((ImportAware) bean).setImportMetadata(importingClass);
}
}
return bean;
}

5、将配置类 ImportResource 注解指定的配置文件路径添加到 ConfigurationClass 对象

1
2
3
4
5
6
7
8
9
AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class < ?extends BeanDefinitionReader > readerClass = importResource.getClass("reader");
for (String resource: resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}

6、将配置类里使用@Bean 的方法添加到 ConfigurationClass 对象

1
2
3
4
Set < MethodMetadata > beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata: beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}

7、如果配置类有父类,回到步骤 1 继续解析父类

1
2
3
4
5
6
7
8
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
// Superclass found, return its annotation metadata and recurse
return sourceClass.getSuperClass();
}
}

8、ConfigurationClass 添加到 ConfigurationClassParser

1
this.configurationClasses.put(configClass, configClass);

9、从 ConfigurationClassParser 取出所有 ConfigurationClass,注册 bd 以及它引入的 bd

ConfigurationClass 可能的来源:

  • @ComponentScan 扫描的@Component 和@Configuration
  • @Import 导入的普通 bean,也可能是 ImportSelector 方式指定的普通 bean

这里注册的 bd 只包括@Import 导入的 bean 和@Bean 方式创建的 bean,@ComponentScan 扫到的 bean 在解析 ConfigurationClass 的过程中已经注册了

  • 【@Bean 方法、ImportBeanDefinitionRegistrar、@ImportResource 指定的配置文件】会添加到其所属的配置类 ConfigurationClass,然后在这里从 ConfigurationClass 取出并注册 bd
1
2
3
4
5
6
7
8
9
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
for (BeanMethod beanMethod: configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}

loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());

注册@Bean 方法指定的 bd 时,如果使用了@Conditional 注解,加载 Condition 类,调用 matches 方法,返回 true 才会注册到 bf

1
2
3
4
5
6
7
8
9
10
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
ConfigurationClass configClass = beanMethod.getConfigurationClass();
MethodMetadata metadata = beanMethod.getMetadata();
String methodName = metadata.getMethodName();
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
}

... ...
}

condition 类我们一般继承 ConfigurationCondition,实现它的方法getConfigurationPhase指定 condition 作用的阶段,对于@Bean 时使用的 condition,我们指定 condition 作用的阶段为ConfigurationPhase.REGISTER_BEAN,这样在注册@Bean 的 bd 时该 condition 才会生效并被回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) {
......

List < Condition > conditions = new ArrayList();
Iterator var4 = this.getConditionClasses(metadata).iterator();

while (var4.hasNext()) {
String[] conditionClasses = (String[]) var4.next();
String[] var6 = conditionClasses;
int var7 = conditionClasses.length;

for (int var8 = 0; var8 < var7; ++var8) {
String conditionClass = var6[var8];
Condition condition = this.getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}

AnnotationAwareOrderComparator.sort(conditions);
var4 = conditions.iterator();

Condition condition;
ConfigurationPhase requiredPhase;
do {
do {
if (!var4.hasNext()) {
return false;
}

condition = (Condition) var4.next();
requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
} while ( requiredPhase != null && requiredPhase != phase );
} while ( condition . matches ( this . context , metadata ));

return true;

}

如果 ConfigurationCondition 的实现类指定作用的阶段为ConfigurationPhase.PARSE_CONFIGURATION,则 condition 在配置类解析阶段生效,如果返回 false,则直接跳过该配置类,也就是说,既不会注册该配置类,也不会注册由该配置类间接引入的 bean

1
2
3
4
5
6
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}
... ...
}

小结:

ConfigurationClassParser 从@Configuration 配置类里二次解析由它引入的 bean,包括@ComponentScan 方式、@Bean 方式和@Import 导入方式,并将@ComponentScan 扫到的 bean 的 bd 注册到 bf
ConfigurationClassPostProcessor 负责把 ConfigurationClassParser 解析得到的@Import 方式导入的 bean 和@Bean 方式注册的 bean 的 bd 注册到 bf。同时,@Import 方式导入的 Bean 能通过 Import 自省获取导入它的配置类上所有注解

简易流程图

详细流程图

springboot start 实现原理

sb 应用的启动类的@SpringBootApplication注解的元注解包括@EnableAutoConfiguration@SpringBootConfiguration,@SpringBootConfiguration 的元注解包括@Configuration,因此 sb 应用的启动类就是一个配置类
@EnableAutoConfiguration 的元注解包括@Import({AutoConfigurationImportSelector.class}),因此 AutoConfigurationImportSelector 能拿到@EnableAutoConfiguration 注解的参数,实现动态注册 bean 到 sb 应用。不过 AutoConfigurationImportSelector 并没有这么做,而是使用 SpringFactoriesLoader 提供的 spi 机制来注册用户的配置类

1
2
3
4
5
protected List < String > getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List < String > configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}

调用工具类SpringFactoriesLoader的 loadFactoryNames 方法,获取 cp 路径下META-INF/spring.factories配置文件里指定 key=this.getSpringFactoriesLoaderFactoryClass()对应的类路径。getSpringFactoriesLoaderFactoryClass()方法的实现如下:

1
2
3
protected Class < ?>getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}

所以,这里的 key=org.springframework.boot.autoconfigure.EnableAutoConfiguration

总结:

SpringBoot 启动类作为配置类,通过@EnableAutoConfiguration的元注解@Import({AutoConfigurationImportSelector.class}),动态注册 cp 路径下META-INF/spring.factories配置文件里 key=org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的 value bean,即用户配置类 bean。这种通过 spi 方式动态注册用户 bean 的机制称为 springboot-starter

因此,如果希望外部 sb 应用能注册自己提供的 bean,可以利用 springboot-starter 机制。只需要创建一个配置类定义外部应用依赖该 starter 的组件 bean,然后在 starter 包的META-INF/spring.factories配置文件里指定 key=org.springframework.boot.autoconfigure.EnableAutoConfiguration,value={配置类全路径}。外部 sb 应用依赖 starter 包后,就会通过 starter 机制注册配置类和由它引入的 bean 到 bf

starter 机制其实也是 sb 提倡的约定大于配置思想的体现,约定好 SpringFactoriesLoader 加载 starter 配置类声明文件的文件名和路径,sb 就会自动注册配置 bean 和由它引入的 bean。否则你定义一个配置类声明文件路径,它又定义一个,增加了复杂度和做决定的成本,通过约定产生的规范减少这种开销,也不失灵活性,所以一定限度的规范约束是能大大提高生产力的!

@Enable 机制动态注册 bean

动态注册外部 bean 其实有很多方式,其实从根上都是基于@Import 注解来做文章。与 starter 机制类似的还有一种@Enable 机制动态注册 bean,它们的区别是 ImportSelector 的实现逻辑不同。

@Enable 机制可以不依赖@EnableAutoConfiguration注解,在包中自定义一个注解如@EnableMyApp,然后把@Import({XXImportSelector.class})作为元注解,XXImportSelector里指定包对外提供的组件 bean。外部应用只要在自己的配置类中加上我们的@EnableMyApp注解,就可以将包下的组件 bean 注册到 bf

其实这种通过自定义@EnableXXX 注解引入 ImportSelector 来动态注册组件 bean 的方式在 Spring 中也有实际案例,@EnableTransactionManagement自动注册事务组件,@EnableCaching自动注册缓存组件

和 starter 机制最大的区别是,starter 是按约定的路径无脑注册 bean,而这种@Enable 机制能通过注解参数在运行时动态决定加载哪些 bean。前面说过,ImportSelector 可以获取引入它的配置类上的所有注解信息,自然也能拿到引入它的@EnableXXX 注解的参数,然后通过注解参数动态控制要注册的 bean。@EnableTransactionManagement@EnableCaching这两个注解都包含参数AdviceMode mode() default AdviceMode.PROXY;,AdviceModeImportSelector 负责拿到这个 mode 枚举,提供模板方法传递给子类 selector,子类基于不同枚举 mode 选择要动态注册哪些组件 bean