SpringBoot(二):springboot自动装配之SPI机制
上篇文章我们介绍了springboot启动过程中涉及的核心类及其功能,我们知道springboot相较于spring的一大特性就是自动装配,那么自动装配是怎么具体实现的呢? 其实在实现自动装配上springboot采用了多种方案结合的,比如基于spring的扩展点的自动属性注入等,还有提供了一套SPI机制让程序自动可插拔的装配。 本文我带大家重点 了解一下SPI机制的实现原理。
1 什么是SPI?
SPI(Service Provider Interface)机制是一种服务发现和加载机制。它允许开发者编写一个服务接口,然后通过在项目中使用服务提供者实现该接口的方式,实现对应的服务功能。
1.1 JDK中的SPI
JDK的SPI机制通过在Classpath中的META-INF/services目录下,创建以服务接口全限定名命名的文件, 文件的内容为实现该接口的具体实现类。当应用程序需要使用该服务时,JDK会自动加载并实例化配置文件中列出的实现类,并提供给应用程序使用。
1.2 Springboot中的SPI
在Spring Boot中,SPI机制允许开发者通过定义接口和实现类的方式,实现对应的功能扩展。通过在META-INF/spring.factories
配置文件中列出实现类, Spring Boot能够自动加载并使用这些扩展点,提供了灵活的定制和扩展能力。
Tip
:我本人在多年的开发经验中也积累了很多通用的starter,比如说通过引入我的starter修改配置文件就可以快速搭建一个web资源认证授权服务器/客户端, 当然这个也是在spring-security oauth2上进一步的封装,需要了解的可以去Git自己获取
// Git代码
https://gitee.com/yeeevip/yeee-memo/tree/master/memo-parent/memo-common/common-auth/common-platform-auth-server
https://github.com/yeeevip/yeee-memo/tree/master/memo-parent/memo-common/common-auth/common-platform-auth-server
2 Springboot的SPI机制是怎么实现的?
2.1 简短概括
- 程序启动,注册配置类处理器
- spring刷新上下文,执行配置类处理器
- 扫描spring.factories将得到的BeanDefinition注册到容器
- spring实例化/初始化这些BeanDefinition
2.2 源码跟踪
- 启动SpringBoot程序,创建应用上下文ApplicationContext
代码语言:javascript复制我们会调用SpringApplication.run启动程序,通常情况下我们会在程序的主启动类上加一个 @SpringBootApplication 注解,追踪这个注解会发现它是一个复杂的聚合注解, 其中内部包含了@Configuration、 @EnableAutoConfiguration ,而 @EnableAutoConfiguration 有集成了 @Import 注解,这个注解对于springboot非常重要!
// 用户定义的程序主启动类,启动程序
@SpringBootApplication
public class SpringbootExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootExampleApplication.class, args);
}
}
// Springboot的主启动类,创建spring上下文
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
ConfigurableApplicationContext context = null;
try {
...
context = createApplicationContext();
...
} catch (Throwable ex) {
...
}
}
}
- 注册BeanFactory后置处理器用于处理配置类上的注解
springboot启动创建应用上下文后,会注册一系列的内置的后置处理器,如ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor、 CommonAnnotationBeanPostProcessor,其中 ConfigurationClassPostProcessor 实现了BeanFactoryPostProcessor是一个BeanFactory级别的处理器用于处理配置类
注册PostProcessor代码如下:
代码语言:javascript复制public class AnnotatedBeanDefinitionReader {
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
...
// 注册各种后置处理器
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
}
// 注册各种后置处理器
public abstract class AnnotationConfigUtils {
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {
...
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));
}
...
return beanDefs;
}
}
- 刷新应用上下文,执行注册的后置处理器
springboot执行refreshContext刷新上下文,本质还是spring上下文的refresh方法,这个方法是spring生命周期的关键核心代码, spring生命周期的大部分扩展点都是在这里执行的。其中在执行容器中未实例化初始化的bean定义之前会执行内置的、用户自定义的所有注册的beanFactory后置处理器 ,这里会执行到ConfigurationClassPostProcessor这个配置类处理器
Tip
:关于Spring的扩展点大概有13个,其中包括一些后置处理器,具体怎么使用需要了解的可以去Git自己获取,这里不再细说
// Git代码
https://gitee.com/yeeevip/yeee-memo/tree/master/learn-example/spring-boot-example/src/main/java/vip/yeee/memo/demo/springboot/extpoint
https://github.com/yeeevip/yeee-memo/tree/master/learn-example/spring-boot-example/src/main/java/vip/yeee/memo/demo/springboot/extpoint
刷新上下文代码如下:
代码语言:javascript复制// Springboot调用refreshContext来执行父类的refresh方法
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
...
try {
...
// 刷新上下文
refreshContext(context);
...
} catch (Throwable ex) {
throw new IllegalStateException(ex);
}
...
return context;
}
}
// 调用spring的AbstractApplicationContext的refresh()方法刷新
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
public void refresh() throws BeansException, IllegalStateException {
...
try {
...
// 执行所有注册的BeanFactory后置处理器
invokeBeanFactoryPostProcessors(beanFactory);
...
// 实例化、初始化所有的单例Bean
finishBeanFactoryInitialization(beanFactory);
} catch (BeansException ex) {
...
}
}
}
- 执行配置类后置处理器后置处理器(SPI核心逻辑)
spring在执行到BeanFactory处理器的生命周期时会执行到ConfigurationClassPostProcessor这个处理器, 执行具体处理逻辑,如配置类上的@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean等注解都会有相应的处理,目的就是将定义的BeanDefinition注册到BeanFactory容器中以便于最终的初始化。 由于用@SpringBootApplication这个注解标记的类即SpringbootExampleApplication.class本身也是个配置类, 这里的话就会处理这个主启动类。
处理配置类的代码:
代码语言:javascript复制public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor {
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// 这里会有我们程序的主启动配置类,也就是SpringbootExampleApplication.class
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
...
// 转换每个配置类
ConfigurationClassParser parser = new ConfigurationClassParser();
...
do {
...
// 处理配置类
parser.parse(candidates);
...
} while (!candidates.isEmpty());
}
}
// 调用parse方法后会执行到doProcessConfigurationClass
class ConfigurationClassParser {
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) {
// 处理配置类的@PropertySource注解
...
processPropertySource(propertySource);
...
// 处理配置类的@ComponentScan注解
...
Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
...
// 处理配置类的@Import注解,这里就是SPI机制核心实现逻辑了
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
// 处理配置类的@ImportResource注解
...
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
...
// 处理配置类的@Bean注解
...
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
...
}
}
processImports这个方法会专门处理 @Import 这个注解, 进入方法发现会执行getImports方法获得@Import导入的类AutoConfigurationImportSelector.class,由于它实现了DeferredImportSelector,所以程序会执行 deferredImportSelectorHandler.handle去处理这个Selector;其实这里也还没有真正开始处理,最终的处理是在调用deferredImportSelectorHandler.process()发起的。
看代码:
代码语言:javascript复制class ConfigurationClassParser {
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates,...) {
...
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
...
// 判断当前的导入的类是不是DeferredImportSelector的子类
if (selector instanceof DeferredImportSelector) {
// 是DeferredImportSelector的子类的话由deferredImportSelectorHandler去处理
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
...
}
...
}
}
public void parse(Set<BeanDefinitionHolder> configCandidates) {
...
// 这里发起真正处理Selector的逻辑
this.deferredImportSelectorHandler.process();
}
}
- 处理AutoConfigurationImportSelector的具体实现步骤如下:
4.1
读取项目路径下所有依赖中的spring.factories文件,加载需要自动配置的类
代码语言:javascript复制跟踪代码发现,执行process()后会执行handler.processGroupImports(),进而会执行grouping.getImports(),这个方法会用类加载器扫码程序classpath下所有jar中的spring.factories 文件,然后会得到所有用EnableAutoConfiguration属性标记的自动配置类
class ConfigurationClassParser {
public Iterable<Group.Entry> getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
// 会扫描jar下的spring.factories中的自动配置类
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}
}
// 具体扫描spring.factories的实现在AutoConfigurationImportSelector.process
public class AutoConfigurationImportSelector implements DeferredImportSelector,... {
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
// getAutoConfigurationEntry这个方法执行
AutoConfigurationEntry autoConfigurationEntry = (deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
...
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
...
// 这里会调用SpringFactoriesLoader.loadFactoryNames读取spring.factories文件
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
...
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 读取key为EnableAutoConfiguration.class的所有自动配置类
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());
}
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
}
4.2
针对所有扫描到的自动配置类再次执行配置类处理逻辑
上面已经从spring.factories中扫描到了所有配置类,紧接着程序同样会执行配置类处理逻辑,具体处理逻辑同第4节开始,同样也是处理这个配置类中的各种注解/方法, 目的就是为了将这些BeanDefinition加入到spring容器中以便于最终初始化。
4.3
将所有通过配置类处理过符合条件的BeanDefinition注册到spring容器中
执行完针对配置类的处理转换后,也就是执行完parse方法后,会再调用reader.loadBeanDefinitions用来把配置类处理后符合条件的BeanDefinition注册到spring容器
看代码:
代码语言:javascript复制public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor {
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
...
do {
...
// 处理配置类
parser.parse(candidates);
...
// parse转换处理完配置类后,然后会把所有处理过符合条件的注册到spring容器
this.reader.loadBeanDefinitions(configClasses);
...
} while (!candidates.isEmpty());
}
}
- 最后就是spring将所有单例BeanDefinition进行实例化、初始化了
代码语言:javascript复制这里spring会把通过各种方式注册的单例BeanDefinition,如通过注解@Component、@Service标记的Bean,以及我们本文探讨的重点通过SPI机制的导入的Bean进行最终的实例化/初始化。 具体的处理过程也是spring的核心之一,有兴趣的小伙伴可以看一下源码,这里不详细介绍了。
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
public void refresh() throws BeansException, IllegalStateException {
...
try {
...
// 执行所有注册的BeanFactory后置处理器
invokeBeanFactoryPostProcessors(beanFactory);
...
// 实例化、初始化所有的单例Bean
finishBeanFactoryInitialization(beanFactory);
} catch (BeansException ex) {
...
}
}
}
2. 总结
好啦,今天的内容就到这里了,通过以上代码分析,带大家了解了springboot的SPI机制实现的原理, 当然本文分析的可能还是比较片面,毕竟springboot在实现自动装配上还不止是SPI一个方案,它还涉及了很多复杂的处理。
文章中如果有问题疑问欢迎小伙伴们提问或者给我指点一下哦,大家一起探讨一下!
下面这个是我多年经验积累的一套脚手架, 里面集成了各种通用的自定义starter,同时也提供了互联网项目中各种中间件/框架的使用demo, 有兴趣的小伙伴可以git看看哦,并且欢迎大佬们指点!!!
代码语言:javascript复制// Git代码
https://gitee.com/yeeevip/yeee-memo
https://github.com/yeeevip/yeee-memo