0%

编写一个自己的SpringBoot Starter

我们用SpringBoot的时候发现有很多starter,比如spring-boot-starter-web等,对于SpringBoot的官方starter,基本上是以spring-boot-starter-xxx来命名的,对于非官方的一些包来说,我们该怎样将自己的包与SpringBoot结合起来呢?在SpringBoot的官方文档中,有这样一章,Creating Your Own Starter,来教我们如何编写自己的starter。

理解@Conditional

我们可以控制一个类在满足某一条件才能进行实例化吗?
在Spring中有一个@Conditional注解和Condition接口,这个接口有一个matches方法,使用者可以重写这个方法,如下所示:

1
2
3
4
5
6
7
public class TestCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return true;
}
}

然后我们就可以把@Conditional(value = TestCondition.class)标注在类上面,表示该类下面的所有@Bean都会启用配置,也可以标注在方法上面,只是对该方法启用配置。

spring-boot-autoconfigure中,SpringBoot官方实现了一系列Condition,用来方便平时的开发,这些实现类都位于org.springframework.boot.autoconfigure.condition,并且每个都提供了对应的注解,下面相关注解及其说明:

注解 说明
@ConditionalOnBean 在BeanFactory已经存在指定Bean
@ConditionalOnMissingBean 在BeanFactory不存在指定Bean
@ConditionalOnClass 在classpath下已存在指定class
@ConditionalOnMissingClass 在classpath下不存在指定class
@ConditionalOnCloudPlatform 指定的云平台已生效(Cloud Foundry platform,Heroku platform,SAP Cloud platform)
@ConditionalOnExpression 指定的SPEL表达式为true时
@ConditionalOnJava 指定的Java版本(前或者后)
@ConditionalOnJndi 指定的JNDI存在
@ConditionalOnNotWebApplication 非web应用
@ConditionalOnWebApplication web环境
@ConditionalOnProperty 指定的property有指定的值
@ConditionalOnResource 在classpath下存在指定的资源
@ConditionalOnSingleCandidate BeanFactory中该类型Bean只有一个或@Primary的只有一个时

基本原理

用过SPI机制的同学可能会清楚一个概念,当一个框架需要动态的扩展能力,给使用者给予充分的扩展能力,那么可能会用到SPI机制。在SpringBoot中,如果我们编写了一个Starter,SpringBoot框架怎么会识别我们的项目呢?所以我们就需要告诉SpringBoot,这里是我写的Starter,你运行时加载吧,类似SPI。SpringBoot正好提供了该功能,被称为Auto-configuration,下面是SpringBoot的官方文档:

Spring Boot checks for the presence of a META-INF/spring.factories file within your published jar. The file should list your configuration classes under the EnableAutoConfiguration key, as shown in the following example:

1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

从上面可以看出,只要我们在我们自己的Stater的META-INF/spring.factories中配置好AutoConfiguration,SpringBoot就可以检测到并且加载啦。这个AutoConfiguration需要用@Configuration标识,代表它是一个配置类,并且结合上面提到的@Conditional及其相关注解,灵活地读取相应配置信息。

实现步骤

SpringBoot官方推荐自定义Starter以xxx-spring-boot-starter命名,并且分两个模块,Autoconfigure模块Starter模块,主要配置在Autoconfigure模块Starter模块是一个空项目。首先在父项目的pom文件加入SpringBoot依赖

1
2
3
4
5
6
7
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

Autoconfigure模块

Autoconfigure模块加入如下依赖:

1
2
3
4
5
6
7
8
9
10
11
<!-- Compile dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

其中spring-boot-configuration-processor主要是用来生成元数据文件(META-INF/spring-configuration-metadata.json )。如果存在该文件,那么SpringBoot会优先读取该元数据文件,提高启动速度。

编写自定义Service

我们编写一个测试Service,其中config是从配置文件读取的:

1
2
3
4
5
6
7
8
9
10
11
public class StarterService {
private String config;

public StarterService(String config) {
this.config = config;
}

public String[] split(String separatorChar) {
return StringUtils.split(this.config, separatorChar);
}
}

编写配置文件读取类

我们用SpringBoot时,会在application.yml中编写一些配置,如果我们要编写自己的配置,那么肯定要读取的,下面这个类用来读取配置文件信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ConfigurationProperties("example.service")
public class StarterServiceProperties {
private boolean enable;
private String config;

public boolean isEnable() {
return enable;
}

public void setEnable(boolean enable) {
this.enable = enable;
}

public String getConfig() {
return config;
}

public void setConfig(String config) {
this.config = config;
}
}

编写AutoConfigure

接下来编写AutoConfigure文件。@ConditionalOnClass代表classpath 中有StarterService这个class,@EnableConfigurationProperties代表启用StarterServiceProperties这个配置类,@ConditionalOnMissingBean代表BeanFactory中没有StarterService这个Bean才实例化,@ConditionalOnProperty根据指定的配置文件指定的值来判断是否加载该Bean。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {

@Autowired
private StarterServiceProperties properties;

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "example.service", value = "enable", havingValue = "true")
StarterService starterService () {
return new StarterService(properties.getConfig());
}

}

配置spring.factories

最后在META-INF文件夹新建spring.factories中加入如下配置:

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=me.mingshan.spring.boot.autoconfigure.StarterAutoConfigure

如果有多个,以,隔开。

Starter模块

Starter模块是个空的项目,只有一个pom文件,加入如下依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>me.mingshan</groupId>
<artifactId>demo-spring-boot-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

测试

新建一个测试项目,将stater依赖引入,在application.yml文件加入如下配置:

1
2
3
4
example:
service:
enable: true
config: qqq,www

新建一个Runner,实现ApplicationRunner接口,当我们启动SpringBoot容器时,就会执行下面该类,输出qqq www

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class Runner implements ApplicationRunner {
@Autowired
private StarterService starterService;

@Override
public void run(ApplicationArguments args) throws Exception {
String[] splitArray = starterService.split(",");
for (String str : splitArray) {
System.out.println(str);
}
}
}

References: