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;
    }
}

配置文件

  1. 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.StartupLoggerAutoConfiguration
    
  2. META-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>

相关链接