储备知识
在理解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 注解通常加在配置类上
- ImportSelector 一般会
@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 | String[] candidateNames = registry.getBeanDefinitionNames(); |
1 | public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) { |
创建一个 ConfigurationBeanParser
对象,专门来解析配置类
1 | ConfigurationClassParser parser = new ConfigurationClassParser( |
ConfigurationBeanParser 读取配置类 bd,创建ConfigurationClass
对象来封装配置类的注解信息,然后开始真正的解析工作
- 解析阶段还会将解析到的一些信息添加到 ConfigurationClass 对象,包括@ImportResource 指定的 bd 配置文件路径、@Import 导入的 ImportBeanDefinitionRegistrar 等
1 | processConfigurationClass(new ConfigurationClass(metadata, beanName)); |
1、如果配置类使用了@PropertySource 注解,读取注解,将应用配置文件添加到 ac 的 environment
1 | for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( |
1 | String resolvedLocation = this.environment.resolveRequiredPlaceholders(location); |
1 | MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources(); |
2、获取并注册@ComponentScan 注解指定的包路径下的所有 bd,对于@Component 和@Configuration 修饰的 bean,都当做配置 bean,将其 bd 封装成 ConfigurationClass 对象,以该 bean 为源头再次解析并注册通过它引入的 bd,即这里会产生方法递归,回到步骤 1
1 | Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( |
3、深度遍历配置类的所有注解,找到所有@Import 注解,将导入类封装为 SourceClass
1 | private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException { |
4、开始处理导入类
1 | processImports(configClass, sourceClass, getImports(sourceClass), true); |
- 如果导入类是 ImportSelector 类型,实例化后回调它的 selectImports 方法获取导入 bean 的类路径,将这些待导入 bean 封装为 sourceClass,回到步骤四继续递归处理这些导入类
- selectImports 方法的入参是引入@Import 注解的配置类上的所有注解信息,因此实现 selectImports 时,可以基于配置类注解信息动态选择要导入哪些 bean
1 | if (candidate.isAssignable(ImportSelector.class)) { |
- 如果导入类是 ImportBeanDefinitionRegistrar,实例化注册器 registrar,然后添加到引入该@Import 注解的配置类的 ConfigurationClass 对象
1 | else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { |
- 如果都不是,说明导入的是普通 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 时作为动态参数传入
- 还会为@Import 导入的 bean 缓存”导入该 Bean 的配置类上所有注解”,如果导入 bean 实现了
1 | else { |
1 | @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { |
5、将配置类 ImportResource 注解指定的配置文件路径添加到 ConfigurationClass 对象
1 | AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); |
6、将配置类里使用@Bean 的方法添加到 ConfigurationClass 对象
1 | Set < MethodMetadata > beanMethods = retrieveBeanMethodMetadata(sourceClass); |
7、如果配置类有父类,回到步骤 1 继续解析父类
1 | if (sourceClass.getMetadata().hasSuperClass()) { |
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 | if (configClass.isImported()) { |
注册@Bean 方法指定的 bd 时,如果使用了@Conditional 注解,加载 Condition 类,调用 matches 方法,返回 true 才会注册到 bf
1 | private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { |
condition 类我们一般继承 ConfigurationCondition,实现它的方法getConfigurationPhase
指定 condition 作用的阶段,对于@Bean 时使用的 condition,我们指定 condition 作用的阶段为ConfigurationPhase.REGISTER_BEAN
,这样在注册@Bean 的 bd 时该 condition 才会生效并被回调
1 | public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) { |
如果 ConfigurationCondition 的实现类指定作用的阶段为ConfigurationPhase.PARSE_CONFIGURATION
,则 condition 在配置类解析阶段生效,如果返回 false,则直接跳过该配置类,也就是说,既不会注册该配置类,也不会注册由该配置类间接引入的 bean
1 | protected void processConfigurationClass(ConfigurationClass configClass) throws IOException { |
小结:
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 | protected List < String > getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { |
调用工具类SpringFactoriesLoader
的 loadFactoryNames 方法,获取 cp 路径下META-INF/spring.factories
配置文件里指定 key=this.getSpringFactoriesLoaderFactoryClass()
对应的类路径。getSpringFactoriesLoaderFactoryClass()
方法的实现如下:
1 | protected Class < ?>getSpringFactoriesLoaderFactoryClass() { |
所以,这里的 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