---
title: 编写Spring Boot Starter
tags:
  - JVM
  - Java
  - Spring
  - Spring Boot
categories:
  - - JVM
    - Java
  - - JVM
    - Spring
date: 2021-04-16 17:43:51
---

在使用Spring Boot的过程中，许多依赖库都是以Spring Boot Starter的形式被引入的。实际上每一个Starter都是一个Spring Boot插件，用来自动装配依赖库工作所需。通过Starter的工作，可以简化Spring框架之前复杂的配置，无论是基于XML的配置，亦或是基于注解的配置。
<!-- more -->

通过使用Starter，一个依赖库的依赖和配置都被集中在了一起。这样一来，主项目就无需再逐个书写其所需要的依赖了，而且也不必再逐一对依赖库进行手工装配了。Spring Boot会自动扫描Starter中的信息并加载、启动相应的配置。例如如果需要在项目中使用JDBC，那么只需要在项目构建配置中引入`spring-boot-starter-jdbc`即可，无需再去逐一寻找要使用JDBC所需要的各个依赖库。

## 一个Starter需要实现的功能

Spring官方所提供的Starter虽然已经能够满足日常项目开发的需要，但是对于一些比较偏门的技术栈，还是需要自行编写Starter的。毕竟每个项目中都书写一大片重复的依赖项，任谁都不会爽。

那么编写一个Starter都需要实现哪些功能呢?

- 整合所需要的依赖项，集成到Starter的构建配置中。
- 提供一套用于初始化依赖项的默认配置，这些默认配置可以在使用时被选择性覆盖。
- 使用自动化配置类对模块中的Bean进行自动装配并注入到Spring容器中。

## Starter的命名规则

Spring官方定义的Starter通常会遵循`spring-boot-starter-{dependency}`的命名格式，例如`spring-boot-starter-redis`。但是Spring官方同样也建议自定义的非官方Starter应该遵循`{dependency}-spring-boot-starter`的格式，这样就会与官方提供的Starter有所区别，但又不至于难以记忆。

## 开发一个Starter

Spring Boot Starter实际上与一个普通的Spring Boot应用没有太多的区别，除了正常的编写构建配置和项目默认配置以外，主要的工作都集中在了编写使用`@Configuration`和`@Bean`修饰的自动装配类，以及使用`spring.factories`文件定义的自动装配类。

在这里就不得不提一下Spring Boot的自动装配。Spring Boot在Spring Framework的基础上定义了一套接口规范，这就是Spring Boot应用在启动的时候会首先扫描所有引用Jar包中的`META-INF/spring.factories`文件，将这个文件中描述的类型信息加载到Spring容器中，并同时完成其中定义的初始化工作。所以只要按照Spring Boot提供的接口规范编写，就可以完成自定义的Starter。

下面以jetcd的Starter为例，给出一个能够自动装配Etcd客户端的示例。首先Spring Boot应用在启动的时候会首先扫描`META-INF/spring.factories`文件来获取装配类。

```ini META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    xyz.archgrid.jetcd.starter.EtcdClientConfiguration
```

如果Starter中有多个自动装配类，可以使用逗号分隔列举在这里。

之后，为了收集Etcd服务的连接信息，还需要定义一个配置项收集类。

```java EtcdConfigurationProperties.java
@ConfigurationProperties(prefix = "etcd")
@Data
public class EtcdConfigurationProperties {
    private Collection<String> endpoints = Collections.emptyList();
    private String username = "";
    private String password = "";
    private String namespace = "";
    private Long retryDelay = 20L;
    private Integer connectTimeout = 60;
}
```

这个配置项收集类没有什么特殊的，与平时在Spring Boot中使用的配置项收集类是一样的。在定义好配置项之后，就是定义最关键的自动装配类了。

```java EtcdClientConfiguration.java
@Configuration
@ConditionalOnClass(Client.class)
@EnableConfigurationProperties(EtcdConfigurationProperties.class)
public class EtcdClientConfiguration {

    private EtcdConfigurationProperties properties;

    public EtcdClientConfiguration(EtcdConfigurationProperties properties) {
        this.properties = properties;
    }

    @Bean
    @Scope("singleton")
    @ConditionalOnMissingBean(Client.class)
    public Client buildEtcdClient() {
        ClientBuilder builder = Client.builder();
        if (properties.getEndpoints().isEmpty()) {
            properties.getEndpoints().add("http://127.0.0.1:2379");
        }
        builder.endpoints(properties.getEndpoints().toArray(new String[0]));
        if (!properties.getUsername().isBlank()) {
            builder.user(ByteSequence.from(properties.getUsername(), StandardCharsets.UTF_8));
        }
        if (!properties.getPassword().isBlank()) {
            builder.password(ByteSequence.from(properties.getPassword(), StandardCharsets.UTF_8));
        }
        if (!properties.getNamespace().isBlank()) {
            builder.namespace(ByteSequence.from(properties.getNamespace(), StandardCharsets.UTF_8));
        }
        builder.retryChronoUnit(ChronoUnit.SECONDS);
        builder.retryDelay(properties.getRetryDelay());
        builder.connectTimeoutMs(properties.getConnectTimeout() * 1000);
        return builder.build();
    }

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean(KV.class)
    public KV buildEtcdKVClient(Client client) {
        return client.getKVClient();
    }
}
```

这个自动装配类其实也就是一个普通的Spring Boot的配置类，只是在它上面增加了一些条件注解。这些条件注解可以根据Spring对象池内的对象来决定是否要使用制动装配类中定义的工厂方法组建新的对象实例。除了示例中使用的`@ConditionalOnClass`和`@ConditionalOnMissingBean`以外，常用的条件注解主要有以下这些。

- `@ConditionalOnMissingBean`，当指定的条件的Bean不存在的时候启动装配。通常用于补充应用中需要的Bean，避免重复装配。
- `@ConditionalOnBean`，当指定的条件的Bean存在的时候启动装配。通常用于根据Bean之间的依赖来避免装配失败。
- `@ConditionalOnMissingClass`，当指定的条件的类不存在的时候启动装配。通常用于在应用中提供替代Bean。
- `@ConditionalOnClass`，当指定的条件的类存在的时候启动装配。通常用于在应用中根据类之间的依赖避免装配失败。
- `@ConditionalOnProperty`，根据指定配置文件中的配置项来确定是否启动装配。
- `@ConditionalOnResource`，根据指定的资源是否存在确定是否启动装配。
- `@ConditionalOnExpression`，根据SpEL表达式的执行结果确定是否启动装配。

## 自动装配机制简述

Spring Boot应用所使用的`@SpringBootApplication`注解，实际上可以被看作是`@Configuration`、`@EnableAutoConfiguration`和`@ComponentScan`三个注解的组合。这三个注解分别用于注册额外的Bean或者导入其他的配置类、启动Spring的自动装配机制和扫描被`@component`注解的Bean。其中`@EnableAutoConfiguration`注解可以配合`AutoConfigurationImportSelector`类来实现自动装配功能。

## 一个小技巧

在IntelliJ IDEA中对Spring Boot应用的`application.properties`或者`application.yml`文件进行编辑的时候，往往可以看到针对目前应用所使用的Starter提供的配置项提示。实际上这是Starter中的文件`META-INF/spring-configuration-metadata.json`发挥的作用，这个文件的内容描述了Starter中所需要的全部配置项的信息。

这个元信息描述文件主要需要编写的内容是各个配置项的名称、类型以及描述，对于配置项的类型，可以直接使Java的类型全称来描述。对于前面这个用于Etcd的Starter来说，可以编写一个下面这样的描述文件。

```json META-INF/spring-configuration-metadata.json
{
    "properties": [
        {
            "name": "etcd.endpoints",
            "type": "java.util.Collection<java.lang.String>",
            "description": "Etcd主机列表"
        },
        {
            "name": "etcd.username",
            "type": "java.lang.String",
            "description": "Etcd主机认证用户名",
            "defaultValue": "" 
        },
        {
            "name": "etcd.password",
            "type": "java.lang.String",
            "description": "Etcd主机认证密码",
            "defaultValue": ""
        },
        {
            "name": "etcd.namespace",
            "type": "java.lang.String",
            "description": "所要操作的默认命名空间",
            "defaultValue": ""
        },
        {
            "name": "etcd.retryDelay",
            "type": "java.lang.Long",
            "description": "连接失败后重试的时间间隔，单位秒",
            "defaultValue": 20
        },
        {
            "name": "etcd.connectTimeout",
            "type": "java.lang.Integer",
            "description": "连接超时时间，单位秒",
            "defaultValue": 60
        }
    ]
}
```
