跳到主要内容

自定义 Starter 开发

当你在开发共享库或者需要为团队提供统一的技术方案时,自定义 Starter 是一个非常强大的工具。本章将详细介绍如何创建自己的 Spring Boot Starter。

什么是自定义 Starter?

Starter 是 Spring Boot 提供的一种便捷方式,它将一组相关的依赖和自动配置打包在一起,让开发者只需添加一个依赖就能快速集成某项技术。

为什么需要自定义 Starter?

在实际开发中,以下场景适合创建自定义 Starter:

  • 公司内部公共组件:统一的日志、认证、监控等基础组件
  • 开源库集成:为第三方库提供 Spring Boot 集成
  • 业务模块复用:将通用业务逻辑封装成可复用模块
  • 简化配置:将复杂的配置逻辑封装,降低使用门槛

Starter 的组成

一个完整的 Starter 通常包含:

组件说明
autoconfigure 模块包含自动配置代码和必要的 API
starter 模块聚合依赖,提供一站式引入
配置属性@ConfigurationProperties 定义可配置项
条件注解控制自动配置的加载时机

创建自定义 Starter

项目结构

对于复杂的 Starter,建议采用双模块结构:

acme-spring-boot/
├── acme-spring-boot-autoconfigure/ # 自动配置模块
│ ├── src/main/java/
│ │ └── com/example/autoconfigure/
│ │ ├── AcmeAutoConfiguration.java
│ │ ├── AcmeProperties.java
│ │ └── AcmeService.java
│ └── src/main/resources/
│ └── META-INF/spring/
│ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports

└── acme-spring-boot-starter/ # Starter 模块
└── pom.xml # 仅包含依赖声明

对于简单的 Starter,可以合并为单模块:

acme-spring-boot-starter/
├── src/main/java/
│ └── com/example/
│ ├── autoconfigure/
│ │ ├── AcmeAutoConfiguration.java
│ │ └── AcmeProperties.java
│ └── AcmeService.java
└── src/main/resources/
└── META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports

命名规范

命名是创建 Starter 的第一步,需要遵循规范:

模块类型命名格式示例
自动配置模块{name}-spring-bootmylib-spring-boot
Starter 模块{name}-spring-boot-startermylib-spring-boot-starter
单模块 Starter{name}-spring-boot-startermylib-spring-boot-starter

重要规则

  1. 不要以 spring-boot 开头,这是官方 Starter 的保留前缀
  2. 使用自己拥有的命名空间,避免与官方冲突
  3. 如果只有一个模块,直接命名为 {name}-spring-boot-starter

创建自动配置模块

1. 定义配置属性

配置属性让用户能够自定义 Starter 的行为:

package com.example.autoconfigure;

import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* Acme 服务配置属性
*/
@ConfigurationProperties(prefix = "acme")
public class AcmeProperties {

/**
* 是否启用 Acme 服务
*/
private boolean enabled = true;

/**
* 服务端点地址
*/
private String endpoint = "http://localhost:8080";

/**
* 连接超时时间
*/
private Duration connectionTimeout = Duration.ofSeconds(5);

/**
* 读取超时时间
*/
private Duration readTimeout = Duration.ofSeconds(30);

/**
* 重试次数
*/
private int retryTimes = 3;

// Getter 和 Setter 方法
public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public String getEndpoint() {
return endpoint;
}

public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}

public Duration getConnectionTimeout() {
return connectionTimeout;
}

public void setConnectionTimeout(Duration connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}

public Duration getReadTimeout() {
return readTimeout;
}

public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}

public int getRetryTimes() {
return retryTimes;
}

public void setRetryTimes(int retryTimes) {
this.retryTimes = retryTimes;
}
}

配置属性文档化规则

规则说明示例
不以 "The" 或 "A" 开头保持简洁"服务端点地址" 而非 "The 服务端点地址"
布尔类型用 "Whether" 开头描述是否启用"Whether to enable acme service"
集合类型用 "Comma-separated list"说明分隔方式"Comma-separated list of hosts"
Duration 类型说明默认单位时间单位说明"If no suffix specified, seconds will be used"

2. 创建核心服务类

package com.example.autoconfigure;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Acme 服务核心实现
*/
public class AcmeService {

private static final Logger log = LoggerFactory.getLogger(AcmeService.class);

private final AcmeProperties properties;

public AcmeService(AcmeProperties properties) {
this.properties = properties;
}

/**
* 发送消息到 Acme 服务
*/
public String sendMessage(String message) {
log.info("Sending message to {}: {}", properties.getEndpoint(), message);

// 模拟发送逻辑
// 实际实现中这里会有 HTTP 调用或其他操作

return "Message sent successfully";
}

/**
* 检查服务是否可用
*/
public boolean isAvailable() {
return properties.isEnabled();
}
}

3. 创建自动配置类

自动配置类是 Starter 的核心,它决定了何时以及如何创建 Bean:

package com.example.autoconfigure;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

/**
* Acme 自动配置类
*/
@AutoConfiguration
@ConditionalOnClass(AcmeService.class) // 类路径中存在 AcmeService 时生效
@EnableConfigurationProperties(AcmeProperties.class) // 启用配置属性
@ConditionalOnProperty(prefix = "acme", name = "enabled",
havingValue = "true", matchIfMissing = true) // 配置启用时生效
public class AcmeAutoConfiguration {

/**
* 创建 AcmeService Bean
* 只有当容器中不存在该 Bean 时才创建
*/
@Bean
@ConditionalOnMissingBean
public AcmeService acmeService(AcmeProperties properties) {
return new AcmeService(properties);
}
}

条件注解说明

注解作用
@AutoConfiguration标识自动配置类
@ConditionalOnClass类路径存在指定类时生效
@ConditionalOnMissingBean容器中不存在指定 Bean 时生效
@ConditionalOnProperty配置属性满足条件时生效
@EnableConfigurationProperties启用配置属性绑定

4. 注册自动配置

src/main/resources/META-INF/spring/ 目录下创建文件:

文件名org.springframework.boot.autoconfigure.AutoConfiguration.imports

内容

com.example.autoconfigure.AcmeAutoConfiguration

每行一个自动配置类的全限定名。可以使用 # 添加注释:

# Acme 自动配置
com.example.autoconfigure.AcmeAutoConfiguration
com.example.autoconfigure.AcmeWebAutoConfiguration

创建 Starter 模块

Starter 模块是一个空 JAR,仅负责聚合依赖:

<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>acme-spring-boot-starter</artifactId>
<version>1.0.0</version>

<name>acme-spring-boot-starter</name>
<description>Acme Spring Boot Starter</description>

<dependencies>
<!-- 核心启动器,必须包含 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- 自动配置模块 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>acme-spring-boot-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>

<!-- 其他必要依赖 -->
<!-- 注意:不要添加可选依赖 -->
</dependencies>
</project>

Starter 依赖原则

  1. 必须直接或间接引用 spring-boot-starter
  2. 不要包含可选依赖,让用户按需添加
  3. 仅包含使用该功能必需的依赖

自动配置模块的 pom.xml

<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>acme-spring-boot-autoconfigure</artifactId>
<version>1.0.0</version>

<name>acme-spring-boot-autoconfigure</name>
<description>Acme Spring Boot AutoConfigure</description>

<dependencies>
<!-- Spring Boot 自动配置依赖 -->
<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>

<!-- 自动配置元数据处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>

<!-- 第三方库依赖,标记为 optional -->
<dependency>
<groupId>com.thirdparty</groupId>
<artifactId>thirdparty-lib</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<!-- 配置注解处理器 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
</path>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<version>${spring-boot.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

关键配置说明

配置作用
spring-boot-autoconfigure自动配置基础支持
spring-boot-configuration-processor生成配置元数据,支持 IDE 提示
spring-boot-autoconfigure-processor生成条件注解元数据,加速启动
<optional>true</optional>标记为可选,不会传递给使用者

条件注解详解

类条件

// 类路径中存在指定类时生效
@ConditionalOnClass(DataSource.class)
public class MyAutoConfiguration {
// ...
}

// 类路径中不存在指定类时生效
@ConditionalOnMissingClass("com.example.OptionalClass")
public class MyAutoConfiguration {
// ...
}

Bean 条件

@Bean
// 容器中不存在该类型 Bean 时创建
@ConditionalOnMissingBean
public MyService myService() {
return new DefaultMyService();
}

@Bean
// 容器中存在指定 Bean 时创建
@ConditionalOnBean(DataSource.class)
public MyRepository myRepository() {
return new MyRepositoryImpl();
}

属性条件

// 属性值为 true 时生效
@ConditionalOnProperty(prefix = "my.feature", name = "enabled",
havingValue = "true", matchIfMissing = true)
public class MyAutoConfiguration {
// ...
}

// 布尔属性的专用注解
@ConditionalOnBooleanProperty(name = "my.feature.enabled")
public class MyAutoConfiguration {
// ...
}

Web 应用条件

// Servlet Web 应用时生效
@ConditionalOnWebApplication(type = Type.SERVLET)
public class MyWebAutoConfiguration {
// ...
}

// 非 Web 应用时生效
@ConditionalOnNotWebApplication
public class MyNonWebAutoConfiguration {
// ...
}

资源条件

// 指定资源存在时生效
@ConditionalOnResource(resources = "classpath:my-config.xml")
public class MyAutoConfiguration {
// ...
}

SpEL 表达式条件

// SpEL 表达式为 true 时生效
@ConditionalOnExpression("${my.feature.enabled:false} and '${my.feature.mode}' == 'advanced'")
public class MyAutoConfiguration {
// ...
}

配置顺序控制

当自动配置类之间有依赖关系时,需要控制加载顺序:

@AutoConfiguration
@AutoConfigureBefore(DataSourceAutoConfiguration.class) // 在指定配置之前加载
public class MyDataSourceAutoConfiguration {
// ...
}

@AutoConfiguration
@AutoConfigureAfter(WebMvcAutoConfiguration.class) // 在指定配置之后加载
public class MyWebAutoConfiguration {
// ...
}

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) // 指定顺序值
public class MyAutoConfiguration {
// ...
}

顺序说明

  • @AutoConfigureBefore:在指定配置类之前加载
  • @AutoConfigureAfter:在指定配置类之后加载
  • @AutoConfigureOrder:数值越小优先级越高

测试自动配置

测试是确保 Starter 正确工作的关键。Spring Boot 提供了 ApplicationContextRunner 来测试自动配置。

基本测试

import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import static org.assertj.core.api.Assertions.assertThat;

class AcmeAutoConfigurationTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AcmeAutoConfiguration.class));

@Test
void defaultServiceCreated() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(AcmeService.class);
assertThat(context.getBean(AcmeService.class)).isNotNull();
});
}

@Test
void serviceNotCreatedWhenDisabled() {
contextRunner
.withPropertyValues("acme.enabled=false")
.run(context -> {
assertThat(context).doesNotHaveBean(AcmeService.class);
});
}

@Test
void customPropertiesApplied() {
contextRunner
.withPropertyValues(
"acme.enabled=true",
"acme.endpoint=http://custom:9090",
"acme.connection-timeout=10s"
)
.run(context -> {
AcmeProperties properties = context.getBean(AcmeProperties.class);
assertThat(properties.getEndpoint()).isEqualTo("http://custom:9090");
assertThat(properties.getConnectionTimeout()).isEqualTo(Duration.ofSeconds(10));
});
}

@Test
void userCanOverrideBean() {
contextRunner
.withUserConfiguration(CustomAcmeConfiguration.class)
.run(context -> {
assertThat(context).hasSingleBean(AcmeService.class);
assertThat(context.getBean("customAcmeService"))
.isSameAs(context.getBean(AcmeService.class));
});
}

// 用户自定义配置
static class CustomAcmeConfiguration {
@Bean
AcmeService customAcmeService() {
return new AcmeService(new AcmeProperties()) {
@Override
public String sendMessage(String message) {
return "Custom: " + message;
}
};
}
}
}

Web 上下文测试

对于需要 Web 环境的配置,使用 WebApplicationContextRunner

import org.springframework.boot.test.context.runner.WebApplicationContextRunner;

class AcmeWebAutoConfigurationTest {

private final WebApplicationContextRunner contextRunner =
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AcmeWebAutoConfiguration.class));

@Test
void webComponentsCreated() {
contextRunner.run(context -> {
assertThat(context).hasBean("acmeController");
});
}
}

模拟类路径缺失

测试当依赖不存在时的行为:

import org.springframework.boot.test.context.FilteredClassLoader;

@Test
void serviceNotCreatedWhenClassNotPresent() {
contextRunner
.withClassLoader(new FilteredClassLoader(AcmeService.class))
.run(context -> {
assertThat(context).doesNotHaveBean(AcmeService.class);
});
}

生成配置元数据

配置元数据让 IDE 能够提供自动补全和文档提示。

自动生成

添加 spring-boot-configuration-processor 依赖后,编译时会自动生成元数据文件。

生成的文件位于:META-INF/spring-configuration-metadata.json

{
"groups": [
{
"name": "acme",
"type": "com.example.autoconfigure.AcmeProperties",
"sourceType": "com.example.autoconfigure.AcmeProperties",
"description": "Acme 服务配置属性"
}
],
"properties": [
{
"name": "acme.enabled",
"type": "java.lang.Boolean",
"description": "是否启用 Acme 服务",
"defaultValue": true
},
{
"name": "acme.endpoint",
"type": "java.lang.String",
"description": "服务端点地址",
"defaultValue": "http://localhost:8080"
},
{
"name": "acme.connection-timeout",
"type": "java.time.Duration",
"description": "连接超时时间",
"defaultValue": "5s"
}
]
}

手动添加元数据

对于无法自动生成的配置,可以手动创建 META-INF/additional-spring-configuration-metadata.json

{
"properties": [
{
"name": "acme.custom-property",
"type": "java.lang.String",
"description": "自定义属性说明"
}
],
"hints": [
{
"name": "acme.log-level",
"values": [
{"value": "debug", "description": "调试级别"},
{"value": "info", "description": "信息级别"},
{"value": "warn", "description": "警告级别"},
{"value": "error", "description": "错误级别"}
]
}
]
}

最佳实践

1. 配置属性命名空间

使用自己拥有的命名空间,避免与官方冲突:

# 推荐:使用自己的前缀
acme:
enabled: true
endpoint: http://localhost:8080

# 避免:使用官方命名空间
server: # 这是 Spring Boot 官方命名空间
port: 8080

2. 合理使用条件注解

// 推荐:条件判断放在类级别
@AutoConfiguration
@ConditionalOnClass(AcmeService.class)
@ConditionalOnProperty(prefix = "acme", name = "enabled", matchIfMissing = true)
public class AcmeAutoConfiguration {
// ...
}

// 推荐:使用 @ConditionalOnMissingBean 允许覆盖
@Bean
@ConditionalOnMissingBean
public AcmeService acmeService(AcmeProperties properties) {
return new AcmeService(properties);
}

3. 依赖标记为 optional

<!-- 自动配置模块中,第三方依赖标记为 optional -->
<dependency>
<groupId>com.thirdparty</groupId>
<artifactId>thirdparty-lib</artifactId>
<optional>true</optional>
</dependency>

这样当用户不需要该功能时,不会引入额外依赖。

4. 提供合理的默认值

@ConfigurationProperties(prefix = "acme")
public class AcmeProperties {

// 提供开箱即用的默认值
private boolean enabled = true;
private String endpoint = "http://localhost:8080";
private Duration timeout = Duration.ofSeconds(30);
private int retryTimes = 3;

// ...
}

5. 编写完整的测试

// 测试默认配置
@Test
void testDefaultConfiguration() { }

// 测试自定义配置
@Test
void testCustomConfiguration() { }

// 测试禁用场景
@Test
void testDisabledConfiguration() { }

// 测试 Bean 覆盖
@Test
void testBeanOverride() { }

// 测试依赖缺失场景
@Test
void testMissingDependency() { }

6. 文档化配置属性

@ConfigurationProperties(prefix = "acme")
public class AcmeProperties {

/**
* 是否启用 Acme 服务。默认为 true。
*/
private boolean enabled = true;

/**
* Acme 服务端点地址。默认为 http://localhost:8080。
*/
private String endpoint = "http://localhost:8080";
}

使用自定义 Starter

创建完成后,在其他项目中使用 Starter 非常简单:

1. 添加依赖

<dependency>
<groupId>com.example</groupId>
<artifactId>acme-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>

2. 配置属性

acme:
enabled: true
endpoint: http://acme-server:8080
connection-timeout: 10s
read-timeout: 60s
retry-times: 5

3. 使用服务

@Service
public class MyService {

@Autowired
private AcmeService acmeService;

public void doSomething() {
String result = acmeService.sendMessage("Hello World");
System.out.println(result);
}
}

4. 自定义 Bean(可选)

如果需要覆盖默认实现:

@Configuration
public class MyConfiguration {

@Bean
public AcmeService acmeService(AcmeProperties properties) {
return new CustomAcmeService(properties);
}
}

小结

本章我们学习了:

  1. Starter 概念:理解 Starter 的作用和组成
  2. 项目结构:双模块和单模块的组织方式
  3. 命名规范:遵循官方命名约定
  4. 自动配置开发:创建配置属性和自动配置类
  5. 条件注解:控制配置加载时机
  6. 测试:使用 ApplicationContextRunner 测试配置
  7. 配置元数据:生成 IDE 提示支持
  8. 最佳实践:命名空间、依赖管理、文档化

练习

  1. 创建一个简单的 Starter,提供时间格式化服务
  2. 为 Starter 添加多个配置属性并生成元数据
  3. 编写完整的单元测试覆盖各种场景
  4. 尝试创建双模块结构的 Starter

参考资源