0%

SpringBoot之自定义Starter

本文介绍如何在SpringBoot下自定义Starter

abstract.png

自定义SDK

SpringBoot框架提供了各种starter方便日常开发使用,底层即是通过Spring SPI机制来实现自动装配的。这里我们来定义一个SDK展示具体过程

POM文件

sdk工程的POM文件如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.aaron</groupId>
<artifactId>sdk1</artifactId>
<version>9.6.6</version>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.6</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
</project>

属性类

然后我们定义一个属性类,实现将配置文件中相关配置项的值绑定到该属性类实例的字段当中。避免通过@Value注解来一个一个注入。示例代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.aaron.sdk1.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@Data
// 用于获取、关联配置文件中指定前缀的配置项所对应的值
@ConfigurationProperties(prefix = AaronSdkConfigProperties.PREFIX)
public class AaronSdkConfigProperties {
public static final String PREFIX = "aaron.sdk.config";

// 对应的配置项名称为 aaron.sdk.config.separator,若未指定,则使用---默认值
private String separator = "---";

// 对于嵌套实现,需添加 @NestedConfigurationProperty 注解
@NestedConfigurationProperty
private PeopleInfo peopleInfo;

@Data
public static class PeopleInfo {
// 对应的配置项名称为 aaron.sdk.config.peopleInfo.firstName
private String firstName;

// 对应的配置项名称为 aaron.sdk.config.peopleInfo.lastName,若未指定,则使用Zhu默认值
private String lastName="Zhu";

// 对应的配置项名称为 aaron.sdk.config.peopleInfo.age
private Integer age;
}
}

Service

作为一个SDK,我们自然需要来提供几个Service来实现些功能。这里提供了HelloService、ByeService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.aaron.sdk1.service;

import com.aaron.sdk1.config.AaronSdkConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;

public class HelloService {

@Autowired
private AaronSdkConfigProperties aaronSdkConfigProperties;

public void hello() {
StringBuilder sb = new StringBuilder();
sb.append("<")
.append(aaronSdkConfigProperties.getPeopleInfo().getFirstName())
.append(".")
.append(aaronSdkConfigProperties.getPeopleInfo().getLastName())
.append(">: Hello. ")
.append("My Age")
.append(" "+aaronSdkConfigProperties.getSeparator() + " ")
.append(aaronSdkConfigProperties.getPeopleInfo().getAge());

System.out.println(sb.toString());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.aaron.sdk1.service;

import com.aaron.sdk1.config.AaronSdkConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;

public class ByeService {

@Autowired
private AaronSdkConfigProperties aaronSdkConfigProperties;

public void bye() {
StringBuilder sb = new StringBuilder();
sb.append("<")
.append(aaronSdkConfigProperties.getPeopleInfo().getFirstName())
.append(".")
.append(aaronSdkConfigProperties.getPeopleInfo().getLastName())
.append(">: Bye. ")
.append("My Age")
.append(" "+aaronSdkConfigProperties.getSeparator() + " ")
.append(aaronSdkConfigProperties.getPeopleInfo().getAge());

System.out.println(sb.toString());
}
}

自动配置类

然后对于上述Serivce,我们提供相应的Java配置类使其进行实例化。同时为了实现对属性类AaronSdkConfigProperties的实例化、并向属性类实例的字段注入配置项的值。还需要在Java配置类上添加@EnableConfigurationProperties注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.aaron.sdk1.config;

import com.aaron.sdk1.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 启用指定的属性类后,进行注入、使用
@EnableConfigurationProperties(AaronSdkConfigProperties.class)
@Configuration
public class HelloServiceAutoConfig {
@Autowired
private AaronSdkConfigProperties aaronSdkConfigProperties;

public HelloServiceAutoConfig() {
System.out.println("HelloServiceAutoConfig: 被实例化");
}

@Bean
public HelloService helloService() {
System.out.println("HelloService: 被实例化");
return new HelloService();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.aaron.sdk1.config;

import com.aaron.sdk1.service.ByeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 启用指定的属性类后,进行注入、使用
@EnableConfigurationProperties(AaronSdkConfigProperties.class)
@Configuration
public class ByeServiceAutoConfig {
@Autowired
private AaronSdkConfigProperties aaronSdkConfigProperties;

public ByeServiceAutoConfig() {
System.out.println("ByeServiceAutoConfig: 被实例化");
}

@Bean
public ByeService byeService() {
System.out.println("ByeService: 被实例化");
return new ByeService();
}
}

spring.factories文件

前面提到,自动装配本质是通过Spring SPI机制实现的。故我们还需在 /src/main/resources/META-INF 路径下创建spring.factories文件。当期望实现自动装配时,需要在spring.factories文件中添加相应的键值对。其中,key是特定的,为org.springframework.boot.autoconfigure.EnableAutoConfiguration;value即为该SDK下期望自动装配的所有Java配置类,当存在多个配置类时,使用逗号进行分隔。当一行较长时,还可以使用 续行符反斜杠\ 来进行换行。文件内容如下所示

1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
com.aaron.sdk1.config.HelloServiceAutoConfig, \
com.aaron.sdk1.config.ByeServiceAutoConfig

工程结构

至此,该SDK就基本完成了,工程结构如下所示

figure 1.png

使用SDK

现在我们将sdk通过maven打包install到本地后,本地的其他项目就可以使用该sdk了。首先在其他项目中的pom中引入sdk的依赖

1
2
3
4
5
<dependency>
<groupId>com.aaron</groupId>
<artifactId>sdk1</artifactId>
<version>9.6.6</version>
</dependency>

然后,编写一个测试类来进行验证。我们期望在其他项目中注入sdk相关的service后能够正常使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.aaronzhu.SpringBoot1.service;

import com.aaron.sdk1.service.ByeService;
import com.aaron.sdk1.service.HelloService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class SdkTest {
@Autowired
private HelloService helloService;

@Autowired
private ByeService byeService;

@Test
public void test1() {
System.out.println("Start Test");
helloService.hello();
byeService.bye();
System.out.println("End Test");
}
}

最后,在其他项目的配置文件application.properties中添加sdk相关的配置项

1
2
3
4
# sdk相关的配置项
aaron.sdk.config.separator=+++
aaron.sdk.config.peopleInfo.firstName=Aaron
aaron.sdk.config.peopleInfo.age=18

首先通过启动日志,可以看到sdk中的相关配置类、service类均被成功实例化

figure 2.png

同时,sdk中的service不仅注入成功,也可以正常被使用

figure 3.png

SDK主动生效

上文,我们采用了Spring SPI机制通过spring.factories文件来实现对sdk中相关配置类的实例化。即所谓的被动生效。因为如果我们不再使用该sdk后,不想让该sdk中相关配置类被实例化。只能手动移除对该sdk的pom依赖。故这里补充介绍如何利用@Import注解实现主动生效

这里我们首先在上文的sdk工程中定义一个组合注解@EnableSdk。可以看到该注解上方通过@Import注解来指定需要实例化的若干个配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.aaron.sdk1.annotation;

import com.aaron.sdk1.config.ByeServiceAutoConfig;
import com.aaron.sdk1.config.HelloServiceAutoConfig;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
// 通过 @Import 注解 指定需要进行实例化的配置类
@Import({HelloServiceAutoConfig.class, ByeServiceAutoConfig.class})
public @interface EnableSdk {
}

同时,这里我们将sdk工程中的spring.factories文件删除。此时sdk的工程结构如下所示

figure 4.png

现在我们先将本地maven仓库的sdk包删除干净。然后对sdk重新打包并install到本地。重新运行其他项目中的测试类SdkTest来进行验证下。通过日志不难看出,即使我们在其他项目中引入了sdk依赖。也并不会对sdk中的Java配置类进行实例化,更无法注入该sdk中相关service来使用

figure 5.png

只有当我们在其他项目的启动类上添加@EnableSdk注解后,此时才会对sdk中的Java配置类进行实例化。即所谓的主动生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.aaronzhu;

import com.aaron.sdk1.annotation.EnableSdk;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// 使能sdk
@EnableSdk
@SpringBootApplication
public class SpringBoot1Application {
public static void main(String[] args) {
SpringApplication.run(SpringBoot1Application.class, args);
}
}

现在,sdk中的相关配置类、service类均被成功实例化。此时,sdk中的service不仅注入成功,也可以正常被使用

figure 6.png

Note

  • Spring Boot从2.7版本开始,推荐在 resources/META-INF/spring/路径下使用 org.springframework.boot.autoconfigure.AutoConfiguration.imports
    文件来指定需要进行自动装配的Java配置类
  • Spring Boot从3.0版本开始,仅支持使用 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件来指定需要进行自动装配的Java配置类。而不再支持通过spring.factories文件来指定需要自动装配的Java配置类
  • 具体地,在 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中指定需要进行自动装配的Java配置类的全限定类名。存在多个Java配置类时按行分隔即可。配置示例如下所示

figure 7.png

请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝