SpringBoot 不同版本开发和使用 Starter 的方法

10 minute

前言

SpringBoot 不同版本开发和使用 Starter 有区别,这里记录下开发的一些前置知识、实现步骤和一些遇到的问题。

SpringBoot 不同版本自动装配的原理及细微区别

开发 Starter 很重要的一个目的就是自动装配,而不同版本的 SpringBoot 在装配细节上有着区别。

SpringBoot 通过 @EnableAutoConfiguration 开启自动装配,通过 SpringFactoriesLoaderImportCandidates 加载 META-INF 下配置文件(不同版本有区别)中的自动配置类实现自动装配,自动配置类其实就是通过 @Conditional 按需加载的配置类,想要其生效必须引入 spring-boot-starter-xxx 包(官方 starter)或者 xxx-spring-boot-starter(第三方 starter) 实现起步依赖。

不同版本具体自动装配的方式有所区别,可通过阅读源码中 org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getCandidateConfigurations 找到区别:

2.6 最后一个版本 2.6.15 通过 SpringFactoriesLoader 加载 META-INF/spring.factories 完成自动装配:

1// 2.6.15
2protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
3    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
4    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
5    return configurations;
6}

从 2.7.0 开始,额外通过 ImportCandidates 加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 找到添加了 @AutoConfiguration 注解的类完成装配,这时还是保证了向下兼容:

1// 2.7.0
2protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
3    List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
4    ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
5    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
6    return configurations;
7}

从 3.0.0 大版本开始,不在使用 SpringFactoriesLoader,需要特别注意:

1// 3.0.0
2protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
3    List<String> configurations = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).getCandidates();
4    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
5    return configurations;
6}

以下用 A 指代 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,B 指代 spring.factories,总结版本间的一些区别如下,开发和使用 Starter 时需要特别注意:

  1. 3.0.0 及以上版本自动装配类通过 A 配置,而 2.6.15 及以下版本通过 B 完成,2.7.0 以上到 3.0.0 之前 AB 均可;
  2. A 配置文件为每行一个 class 的格式,B 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration=class1, class2, ... 的格式
  3. 3.0.0 及以上版本自动装配类需要添加 @AutoConfiguration 注解,而 2.6.15 及以下版本对应是添加 @Configuration,2.7.0 以上到 3.0.0 则根据使用情况而定即可

至于为什么将自动配置类文件进行更改?可以从 Github 上的这个 issue 找到答案:Move away from spring.factories for auto-configurations,阅读后可以总结一下,就是为了避免在配置文件 spring.factories 中手动添加类,通过 @AutoConfiguration 注解自动完成配置,而具体实现是要让编译器扫描添加了 @AutoConfiguration 的类并自动生成 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,但是这个功能还未实现,至少在 2024.2 最新版本 3.2.2 还未实现,可通过这个 issue 追踪进展。

不同版本 Starter 使用上需要注意的地方

了解前文对于原理的解释后,就可以总结出一些开发 starter 时需要注意的地方了。

如果项目使用了 SpringBoot 3.X,但是调用了一个第三方的通过 2.X 开发的 Starter,且是通过 spring.factories 加载类,那么将无法完成自动装配,需要手动配置相关 bean 完成,比如使用 rocketmq-spring-boot-starter-2.2.3 将无法自动装配 RocketMQTemplate,通过如下代码解决即可:

 1@Configuration
 2public class RocketMQConfig {
 3    @Value("${rocketmq.producer.group}")
 4    private String producerGroup;
 5    @Value("${rocketmq.name-server}")
 6    private String namesrvAddr;
 7
 8    @Bean(name = "rocketMQTemplate")
 9    public RocketMQTemplate rocketMQTemplate() {
10        RocketMQTemplate template = new RocketMQTemplate();
11        DefaultMQProducer producer = new DefaultMQProducer();
12        producer.setProducerGroup(producerGroup);
13        producer.setNamesrvAddr(namesrvAddr);
14        template.setProducer(producer);
15        return template;
16    }
17}

通过 @Bean@Configuration 完成 RocketMQTemplate 的注入。

开发一个适应性更好的 Starter

为了让所有版本都能自动完成自动装配,我们可以同时适应两种自动装配方法。注意 @AutoConfiguration 也包含了 @Configuration 注解,所以可以很简单的完成适应,只是需要多准备一份配置文件。

下面使用 3.2.2 版本的 SpringBoot 开发一个模拟式的邮件服务 Starter:

新建项目 email-spring-boot

编辑 pom.xml:

 1<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 2         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 3    <modelVersion>4.0.0</modelVersion>
 4
 5    <groupId>xdu.zh</groupId>
 6    <artifactId>email-spring-boot</artifactId>
 7    <version>0.0.1</version>
 8    <packaging>pom</packaging>
 9
10    <modules>
11        <module>email-spring-boot-autoconfigure</module>
12        <module>email-spring-boot-starter</module>
13    </modules>
14</project>

新建子模块 email-spring-boot-autoconfigure

编辑 pom.xml:

 1<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 2         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 3    <modelVersion>4.0.0</modelVersion>
 4    <parent>
 5        <groupId>xdu.zh</groupId>
 6        <artifactId>email-spring-boot</artifactId>
 7        <version>0.0.1</version>
 8    </parent>
 9    <artifactId>email-spring-boot-autoconfigure</artifactId>
10    <version>0.0.1</version>
11    <packaging>jar</packaging>
12
13    <dependencyManagement>
14        <dependencies>
15            <dependency>
16                <groupId>org.springframework.boot</groupId>
17                <artifactId>spring-boot-dependencies</artifactId>
18                <version>3.2.2</version>
19                <type>pom</type>
20                <scope>import</scope>
21            </dependency>
22        </dependencies>
23    </dependencyManagement>
24
25    <dependencies>
26        <!-- Spring Boot自动配置依赖 -->
27        <dependency>
28            <groupId>org.springframework.boot</groupId>
29            <artifactId>spring-boot-autoconfigure</artifactId>
30        </dependency>
31        <!-- 元数据配置处理器 -->
32        <dependency>
33            <groupId>org.springframework.boot</groupId>
34            <artifactId>spring-boot-configuration-processor</artifactId>
35            <optional>true</optional>
36        </dependency>
37    </dependencies>
38</project>

通过 EmailProperties 定义一些自动配置项,指定配置前缀为 email.service

 1@ConfigurationProperties("email.service")
 2public class EmailProperties {
 3    private boolean enable = true;
 4    private String host;
 5    private Integer port;
 6    private String name;
 7    private String password;
 8    
 9    // getter and setter...
10}

具体邮件服务实现:

 1public class EmailService {
 2    private final EmailProperties emailProperties;
 3
 4    public EmailService(EmailProperties emailProperties) {
 5        this.emailProperties = emailProperties;
 6    }
 7
 8    public void send(String content) {
 9        System.out.println("开始发送邮件:");
10        System.out.println(String.format("基本信息: host: %s, port: %s", emailProperties.getHost(), emailProperties.getPort()));
11        System.out.println("发送内容: " + content);
12        System.out.println("发送成功!");
13    }
14}

实现自动配置,当存在 EmailService.classemail.servicetrue 且容器中不存在 EmailService 这个 bean 时自动装配 EmailService

 1/**
 2 * @author zh
 3 * @date 2024/2/24
 4 */
 5@AutoConfiguration
 6@ConditionalOnClass(EmailService.class)
 7@EnableConfigurationProperties(value = EmailProperties.class)
 8public class EmailAutoConfiguration {
 9    @Bean
10    @ConditionalOnMissingBean
11    @ConditionalOnProperty(prefix = "email.service", value = "enable", havingValue = "true")
12    public EmailService mailService(EmailProperties mailProperties) {
13        return new EmailService(mailProperties);
14    }
15}

编辑 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

1xdu.zh.EmailAutoConfiguration

编辑 META-INF/spring.factories:

1org.springframework.boot.autoconfigure.EnableAutoConfiguration=xdu.zh.EmailAutoConfiguration

新建子模块 email-spring-boot-starter

编辑 pom.xml:

 1<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 2         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 3    <modelVersion>4.0.0</modelVersion>
 4    <parent>
 5        <groupId>xdu.zh</groupId>
 6        <artifactId>email-spring-boot</artifactId>
 7        <version>0.0.1</version>
 8    </parent>
 9    <artifactId>email-spring-boot-starter</artifactId>
10    <packaging>jar</packaging>
11
12    <dependencies>
13        <dependency>
14            <groupId>xdu.zh</groupId>
15            <artifactId>email-spring-boot-autoconfigure</artifactId>
16            <version>0.0.1</version>
17        </dependency>
18    </dependencies>
19</project>

该模块只完成封装,包含 email-spring-boot-autoconfigure 模块,以及其它一些可能的依赖项(可选),调用 starter 时只需引入此依赖。

进行多版本功能测试

为进行更实际的测试,在此之前将 email-spring-boot 进行发布,在 IDEA 内 maven 命令行面板通过 mvn install 命令安装到本地 maven 仓库。

接着新建测试项目,编辑 pom.xml

 1<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 2         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 3    <modelVersion>4.0.0</modelVersion>
 4
 5    <groupId>xdu.test</groupId>
 6    <artifactId>test</artifactId>
 7    <version>0.0.1</version>
 8    <packaging>jar</packaging>
 9
10    <parent>
11        <groupId>org.springframework.boot</groupId>
12        <artifactId>spring-boot-starter-parent</artifactId>
13<!--        <version>3.0.0</version>-->
14<!--        <version>2.7.0</version>-->
15        <version>2.6.15</version>
16        <relativePath/>
17    </parent>
18
19    <dependencies>
20        <dependency>
21            <groupId>xdu.zh</groupId>
22            <artifactId>email-spring-boot-starter</artifactId>
23            <version>0.0.1</version>
24        </dependency>
25    </dependencies>
26</project>

进行一些必要配置:

1# resources/application.properties
2email.service.enable=true
3email.service.host=mail.qq.com
4email.service.port=965
5email.service.name=admin
6email.service.password=admin

新建 Spring 应用,注入 EmailService,执行 send 方法进行功能测试:

 1@SpringBootApplication
 2public class EmailStarterTestApplication implements CommandLineRunner {
 3    @Autowired
 4    private EmailService emailService;
 5
 6    public static void main(String[] args) {
 7        SpringApplication.run(EmailStarterTestApplication.class, args);
 8    }
 9
10    @Override
11    public void run(String... args) throws Exception {
12        emailService.send("test");
13    }
14}

测试结果:

1...
2开始发送邮件:
3基本信息: host: mail.qq.com, port: 965
4发送内容: test
5发送成功!

再分别将 SpringBoot 版本改为 2.7.0、3.0.0,测试均通过。

参考来源