SpringBoot(二)— SPI和starter优化器原理
算是一篇SrpingBoot自动配置原理解析的前置文章。首先从源码角度回顾了早期的 spring.factories SPI 加载机制,接着分析了 Spring Boot 2.7.x 引入的基于注解粒度的 .imports 文件机制,展示了其在自动配置解耦方面的优势。并介绍了两个常被 starter 使用的辅助组件:
- spring-boot-configuration-processor:生成配置元数据,IDE使用
- spring-boot-autoconfigure-processor:加速自动配置加载
最后,结合自定义 starter 的实践,附上了完整的测试用例,验证上述机制在实际开发中的可用性
SPI机制
可先看看这篇文章,了解下如何从ClassLoader的classpath读取到指定资源
SpringFactoriesLoader
SpringBoot 早期采用的 SPI 扩展机制实现,整体加载逻辑相对简单,负责从指定 ClassLoader 的所有 classpath 路径中加载并缓存META-INF/spring.factories 文件
这个文件本质上是一个 Properties 格式的配置文件,支持一个 key 对应多个 value,多个 value 之间使用英文逗号分隔
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
/**
* 加载并缓存指定ClassLoader下的所有META-INF/spring.factories里的配置
*/
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
// 从缓存里取
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
// 返回classpath下所有的META-INF/spring.factories文件URL形式
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 读取为Properties
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils
.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// 去重后再转化为不可修改的List
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
// 缓存
cache.put(classLoader, result);
} catch (IOException ex) {
throw new IllegalArgumentException(
"Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
}
ImportCandidates
是SpringBoot 2.7.x版本引入的新的SPI,替代早期的 META-INF/spring.factories 里的部分配置。传统的 spring.factories 文件中将所有自动配置、监听器等内容集中在一个文件中,内容有些杂。为了更加精细化,Spring Boot 引入了基于注解粒度的 .imports 配置机制:每个注解对应一个独立的配置文件,配置文件名为META-INF/spring/注解全类名.imports
但SpringBoot现阶段也就@AutoConfiguration和@ManagementContextConfiguration这两个注解用了这个SPI
以常用的@AutoConfiguration为例,其配置文件名如下,是专门替换META-INF/spring.factories中org.springframework.boot.autoconfigure.EnableAutoConfiguration的配置
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
public final class ImportCandidates implements Iterable<String> {
private static final String LOCATION = "META-INF/spring/%s.imports";
// 注释符号
private static final String COMMENT_START = "#";
private final List<String> candidates;
// 支持迭代
@Override
public Iterator<String> iterator() {
return this.candidates.iterator();
}
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
// annotation一般为注解全类名
Assert.notNull(annotation, "'annotation' must not be null");
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format(LOCATION, annotation.getName());
// 从classLoader的classpath下查找指定文件URL
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 解析文件(每行为一个class,并跳过注释)
importCandidates.addAll(readCandidateConfigurations(url));
}
return new ImportCandidates(importCandidates);
}
}
starter优化器
spring-boot-configuration-processor
引入组件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional> <!-- 避免被传递依赖 -->
</dependency>
介绍
这个组件对项目的运行时没有任何影响,它的作用如下:
会在编译期 自动解析使用了
@ConfigurationProperties等注解的配置类,将配置项的 属性名、类型、默认值、Javadoc 注释 等信息生成到
META-INF/spring-configuration-metadata.json文件中
这个文件主要供IDE使用(如IDEA),用于在编辑 application.yml时提供配置自动补全、类型校验和快速跳转到对应配置类,所以也没必要把这个依赖传递下去
spring-boot-autoconfigure-processor
引入组件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
介绍
一个作用在编译期,用于扫描自动配置类上的部分注解,将解析结果提前生成到META-INF/spring-autoconfigure-metadata.properties文件
通过解析工具AutoConfigureAnnotationProcessor可知,支持的注解如下
@SupportedAnnotationTypes({ "org.springframework.boot.autoconfigure.condition.ConditionalOnClass",
"org.springframework.boot.autoconfigure.condition.ConditionalOnBean",
"org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate",
"org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication",
"org.springframework.boot.autoconfigure.AutoConfigureBefore",
"org.springframework.boot.autoconfigure.AutoConfigureAfter",
"org.springframework.boot.autoconfigure.AutoConfigureOrder",
"org.springframework.boot.autoconfigure.AutoConfiguration" })
META-INF/spring-autoconfigure-metadata.properties文件内部信息格式如下,每一行表示某个自动配置类的某个注解信息:
配置类全类名.上述注解简单名=注解值
site.xxx.Demo.ConditionalOnClass=org.springframework.boot.context.event.ApplicationReadyEvent
该文件会在SpringBoot启动时被AutoConfigurationMetadataLoader加载,这种预解析的结果比反射或asm实时解析相关注解更快,以此来加速程序的启动。
实现一个starter
一个简单的starter,主要目的是在项目成功启动后打印成功信息和耗费时间的日志,且提供可配置的开关和时间格式。时间统计方式:ApplicationStartingEvent到ApplicationReadyEvent这段时间
自动配置类
@AutoConfiguration
@AutoConfigureAfter(name = "org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration")
@EnableConfigurationProperties(StartupLoggerProperties.class)
@ConditionalOnClass(ApplicationReadyEvent.class)
@ConditionalOnProperty(prefix = "startup.logger", name = "enabled", havingValue = "true", matchIfMissing = true)
public class StartupLoggerAutoConfiguration {
@Bean
public ApplicationListener<ApplicationReadyEvent> startupLoggerListener(StartupLoggerProperties props) {
return event -> {
long cost = StartupCostTimeRecorder.cost();
String timeStr = props.getTimeUnit().equals("s") ? (cost / 1000.0 + "s") : cost + "ms";
String appName = event.getApplicationContext().getEnvironment().getProperty("spring.application.name", "Application");
Logger log = LoggerFactory.getLogger("StartupLogger");
log.info("========================================================");
log.info("========================================================");
log.info("======= {} startup successful, cost time:{} =======", appName, timeStr);
log.info("========================================================");
log.info("========================================================");
};
}
}
开关配置
@ConfigurationProperties("startup.logger")
public class StartupLoggerProperties {
/**
* 是否开启
*/
private boolean enabled = true;
/**
* 时间格式:ms, s
*/
private String timeUnit = "ms";
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getTimeUnit() {
return timeUnit;
}
public void setTimeUnit(String timeUnit) {
this.timeUnit = timeUnit;
}
}
启动时间监听器
public class StartupStartListener implements ApplicationListener<ApplicationStartingEvent> {
@Override
public void onApplicationEvent(ApplicationStartingEvent event) {
StartupCostTimeRecorder.recordStart();
}
}
时间记录实体类
public class StartupCostTimeRecorder {
private static Long startTime = null;
public static void recordStart() {
startTime = System.currentTimeMillis();
}
public static long cost() {
return System.currentTimeMillis() - startTime;
}
}
配置文件
META-INF/spring.factories
org.springframework.context.ApplicationListener=\ site.shanzhao.startup.logger.config.StartupStartListener # 还是兼容下2.7.x版本以下的自动配置 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ site.shanzhao.startup.logger.config.StartupLoggerAutoConfigurationMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
site.shanzhao.startup.logger.config.StartupLoggerAutoConfiguration
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>