title: SpringBoot 源码解析 —— 自动装配的奥秘(DeferredImportSelector)
date: 2021/01/15 09:22
remark: SpringBoot 版本为 2.2.6, Spring 版本为 5.2.5
简介
SpringBoot 的自动装配与 @Configuration 注解的处理类 ConfigurationClassPostProcessor 息息相关,所以建议先看下这篇文章再继续向下看。
上图中的第 576-578 行就是今天我们要将的重点:DeferredImportSelector。
继续将之前,我们先了解一下 spring.factories:
spring.factories
spring.factories 是 Spring 仿造 Java SPI 实现的一种类加载机制。它在 META-INF/spring.factories 文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。这种自定义的SPI机制是 Spring Boot Starter 实现的基础。
spring-core 包里定义了 SpringFactoriesLoader 类,这个类实现了检索META-INF/ spring.factories 文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
- loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表。
- loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表。
上面两个方法的关键都是从指定的 ClassLoader 中获取 spring.factories 文件,并解析得到类名列表,具体代码如下:
private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
从代码中可以看到,在这个方法中会遍历类路径下的所有 Jar 包中的 spring.factories 文件。spring.factories 是通过 Properties 解析得到的,所以我们在写文件中的内容都是按照下面这种方式配置的,如果一个接口希望配置多个实现类,可以用","分割。
com.xxx.interface=com.xxx.classname
在日常工作中,我们可能需要实现一些SDK 或者Sring boot starter 给别人用的时候,我们就可以使用Factories机制,但是 Factories 机制可以让 SDK 或者 Stater 的使用只需要很少或者不需要进行配置,只需要在服务中引入我们的 Jar 包即可。
spring-autoconfigure-metadata.properties 文件示例:
# 配合上图中的 OnClassCondition 使用,表示引入 RabbitAutoConfiguration 配置类需要在类路径中存在 Channel 和 RabbitTemplate。
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.ConditionalOnClass=com.rabbitmq.client.Channel,org.springframework.amqp.rabbit.core.RabbitTemplate
为什么不直接在配置类上用 @Condition 注解来实现呢?
官方说,这样可以增加效率。
更多可以参考这篇文章:Springboot自动装配之spring-autoconfigure-metadata.properties和spring.factories
开始吧 @SpringBootApplication
本部分测试demo,虽然没啥东西。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // 其实就是 @Configuration,只不过把它的 proxyBeanMethods 属性的默认值改成了 false
@EnableAutoConfiguration // 重点
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@EnableAutoConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage // 他也 Import 一个类,不过不是本文重点
@Import(AutoConfigurationImportSelector.class) // 重点
public @interface EnableAutoConfiguration {
AutoConfigurationImportSelector
我们看到了他继承了 DeferredImportSelector,好,我们回到 @Configuration 的处理类 ConfigurationClassPostProcessor 中。
看下 deferredImportSelectorHandler#handle()
那么后面是在哪里调用的这些“延迟的” ImportSelector 的呢?
看下 deferredImportSelectorHandler#process()
tag1 handler::register
class ConfigurationClassParser {
...
private class DeferredImportSelectorGroupingHandler {
// key:组类型(在这里 AutoConfigurationGroup) value:组
private final Map
这里一层套一层的可能看着有点乱,看下面的这张图应该能捋清楚一点:
tag2 handler.processGroupImports()
先看下 entry 是啥吧:
class Entry {
// 引入当前类的配置类元数据,这里就是 appBootstrap 的注解元数据
private final AnnotationMetadata metadata;
// 引入类的全类名,例如:org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
private final String importClassName;
tag 到重点了 AutoConfigurationImportSelector#getAutoConfigurationEntry()
看下 ThreadedOutcomesResolver 吧: