自定义 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-boot | mylib-spring-boot |
| Starter 模块 | {name}-spring-boot-starter | mylib-spring-boot-starter |
| 单模块 Starter | {name}-spring-boot-starter | mylib-spring-boot-starter |
重要规则:
- 不要以
spring-boot开头,这是官方 Starter 的保留前缀 - 使用自己拥有的命名空间,避免与官方冲突
- 如果只有一个模块,直接命名为
{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 依赖原则:
- 必须直接或间接引用
spring-boot-starter - 不要包含可选依赖,让用户按需添加
- 仅包含使用该功能必需的依赖
自动配置模块的 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);
}
}
小结
本章我们学习了:
- Starter 概念:理解 Starter 的作用和组成
- 项目结构:双模块和单模块的组织方式
- 命名规范:遵循官方命名约定
- 自动配置开发:创建配置属性和自动配置类
- 条件注解:控制配置加载时机
- 测试:使用 ApplicationContextRunner 测试配置
- 配置元数据:生成 IDE 提示支持
- 最佳实践:命名空间、依赖管理、文档化
练习
- 创建一个简单的 Starter,提供时间格式化服务
- 为 Starter 添加多个配置属性并生成元数据
- 编写完整的单元测试覆盖各种场景
- 尝试创建双模块结构的 Starter