Spring Boot Starter开发指南
约 1173 字大约 4 分钟
spring-bootstarter
2025-03-27
概述
Spring Boot Starter 是 Spring Boot 的核心特性之一,它将一组相关依赖和自动配置打包在一起,使得开发者只需引入一个 Starter 依赖即可获得开箱即用的功能。本文将详细讲解如何从零开发一个自定义 Starter,涵盖命名规范、自动配置、属性绑定、条件装配和测试。
Starter 的工作原理
命名规范
| 类型 | 命名格式 | 示例 |
|---|---|---|
| 官方 Starter | spring-boot-starter-{name} | spring-boot-starter-web |
| 第三方 Starter | {name}-spring-boot-starter | mybatis-spring-boot-starter |
自定义 Starter 通常拆分为两个模块:
my-feature-spring-boot-starter (Starter模块:只包含依赖)
my-feature-spring-boot-autoconfigure (自动配置模块:包含代码)小型项目也可以合并为一个模块。
项目结构
my-sms-spring-boot-starter/
├── pom.xml
└── src/main/java/com/example/sms/
├── autoconfigure/
│ ├── SmsAutoConfiguration.java
│ └── SmsProperties.java
├── SmsClient.java
├── SmsTemplate.java
└── resources/
└── META-INF/
├── spring.factories (Spring Boot 2.x)
└── spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports (Spring Boot 3.x)实战:开发短信服务 Starter
1. 定义配置属性
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
/**
* 是否启用短信服务
*/
private boolean enabled = true;
/**
* 短信服务商:aliyun / tencent
*/
private String provider = "aliyun";
/**
* Access Key ID
*/
private String accessKeyId;
/**
* Access Key Secret
*/
private String accessKeySecret;
/**
* 短信签名
*/
private String signName;
/**
* 默认模板编码
*/
private String defaultTemplateCode;
/**
* 发送超时时间(毫秒)
*/
private int timeout = 5000;
// getters and setters...
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getProvider() { return provider; }
public void setProvider(String provider) { this.provider = provider; }
public String getAccessKeyId() { return accessKeyId; }
public void setAccessKeyId(String accessKeyId) { this.accessKeyId = accessKeyId; }
public String getAccessKeySecret() { return accessKeySecret; }
public void setAccessKeySecret(String accessKeySecret) { this.accessKeySecret = accessKeySecret; }
public String getSignName() { return signName; }
public void setSignName(String signName) { this.signName = signName; }
public String getDefaultTemplateCode() { return defaultTemplateCode; }
public void setDefaultTemplateCode(String defaultTemplateCode) {
this.defaultTemplateCode = defaultTemplateCode;
}
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
}对应的 application.yml 配置:
sms:
enabled: true
provider: aliyun
access-key-id: ${SMS_ACCESS_KEY_ID}
access-key-secret: ${SMS_ACCESS_KEY_SECRET}
sign-name: "MyApp"
default-template-code: "SMS_001"
timeout: 30002. 定义核心服务类
public class SmsTemplate {
private final SmsClient smsClient;
private final SmsProperties properties;
public SmsTemplate(SmsClient smsClient, SmsProperties properties) {
this.smsClient = smsClient;
this.properties = properties;
}
/**
* 发送短信
*/
public SmsResult send(String phoneNumber, String templateCode,
Map<String, String> templateParams) {
return smsClient.send(SmsRequest.builder()
.phoneNumber(phoneNumber)
.signName(properties.getSignName())
.templateCode(templateCode)
.templateParams(templateParams)
.build());
}
/**
* 使用默认模板发送
*/
public SmsResult send(String phoneNumber, Map<String, String> templateParams) {
return send(phoneNumber, properties.getDefaultTemplateCode(), templateParams);
}
/**
* 批量发送
*/
public List<SmsResult> batchSend(List<String> phoneNumbers, String templateCode,
Map<String, String> templateParams) {
return phoneNumbers.stream()
.map(phone -> send(phone, templateCode, templateParams))
.toList();
}
}3. 编写自动配置类
@AutoConfiguration
@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true",
matchIfMissing = true)
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "sms", name = "provider", havingValue = "aliyun",
matchIfMissing = true)
public SmsClient aliyunSmsClient(SmsProperties properties) {
return new AliyunSmsClient(
properties.getAccessKeyId(),
properties.getAccessKeySecret(),
properties.getTimeout()
);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "sms", name = "provider", havingValue = "tencent")
public SmsClient tencentSmsClient(SmsProperties properties) {
return new TencentSmsClient(
properties.getAccessKeyId(),
properties.getAccessKeySecret(),
properties.getTimeout()
);
}
@Bean
@ConditionalOnMissingBean
public SmsTemplate smsTemplate(SmsClient smsClient, SmsProperties properties) {
return new SmsTemplate(smsClient, properties);
}
}4. 注册自动配置
Spring Boot 3.x(推荐):
创建文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.example.sms.autoconfigure.SmsAutoConfigurationSpring Boot 2.x 兼容:
创建文件 META-INF/spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sms.autoconfigure.SmsAutoConfiguration5. 提供配置提示(IDE自动补全)
创建 META-INF/additional-spring-configuration-metadata.json:
{
"properties": [
{
"name": "sms.enabled",
"type": "java.lang.Boolean",
"description": "Enable SMS service.",
"defaultValue": true
},
{
"name": "sms.provider",
"type": "java.lang.String",
"description": "SMS provider: aliyun or tencent.",
"defaultValue": "aliyun"
}
],
"hints": [
{
"name": "sms.provider",
"values": [
{ "value": "aliyun", "description": "Alibaba Cloud SMS" },
{ "value": "tencent", "description": "Tencent Cloud SMS" }
]
}
]
}测试自动配置
class SmsAutoConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SmsAutoConfiguration.class));
@Test
void shouldCreateSmsTemplateByDefault() {
contextRunner
.withPropertyValues(
"sms.access-key-id=test-id",
"sms.access-key-secret=test-secret"
)
.run(context -> {
assertThat(context).hasSingleBean(SmsTemplate.class);
assertThat(context).hasSingleBean(SmsClient.class);
assertThat(context.getBean(SmsClient.class))
.isInstanceOf(AliyunSmsClient.class);
});
}
@Test
void shouldUseTencentClientWhenConfigured() {
contextRunner
.withPropertyValues(
"sms.provider=tencent",
"sms.access-key-id=test-id",
"sms.access-key-secret=test-secret"
)
.run(context -> {
assertThat(context.getBean(SmsClient.class))
.isInstanceOf(TencentSmsClient.class);
});
}
@Test
void shouldNotCreateBeansWhenDisabled() {
contextRunner
.withPropertyValues("sms.enabled=false")
.run(context -> {
assertThat(context).doesNotHaveBean(SmsTemplate.class);
assertThat(context).doesNotHaveBean(SmsClient.class);
});
}
@Test
void shouldBackOffWhenUserDefinesOwnBean() {
contextRunner
.withUserConfiguration(CustomSmsConfig.class)
.withPropertyValues(
"sms.access-key-id=test-id",
"sms.access-key-secret=test-secret"
)
.run(context -> {
assertThat(context).hasSingleBean(SmsClient.class);
assertThat(context.getBean(SmsClient.class))
.isInstanceOf(CustomSmsClient.class);
});
}
@Configuration
static class CustomSmsConfig {
@Bean
SmsClient smsClient() {
return new CustomSmsClient();
}
}
}pom.xml 依赖配置
<project>
<groupId>com.example</groupId>
<artifactId>my-sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>总结
开发自定义 Spring Boot Starter 的核心步骤:定义 @ConfigurationProperties 绑定配置、编写 @AutoConfiguration 自动配置类并使用条件注解控制装配、通过标准文件注册自动配置、使用 ApplicationContextRunner 编写测试。遵循 @ConditionalOnMissingBean 的退让原则,确保用户可以覆盖默认配置。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于