自定义 Starter 实战
概述
Spring Boot Starter 是 Spring Boot 生态中核心的自动配置机制。通过封装一组相关的依赖和自动配置,Starter 允许开发者通过简单的 pom.xml 坐标引入,就能获得开箱即用的功能,而无需手动编写大量配置代码。本文将从零开始,以短信发送服务为例,完整演示如何开发一个生产级的自定义 Starter。
Starter 命名规范
Spring Boot 官方对 Starter 命名有明确约定,遵守这些规范有助于生态兼容性和团队理解。
官方 Starter
由 Spring 官方维护的 Starter 遵循 spring-boot-starter-{模块名} 的格式:
spring-boot-starter-web
spring-boot-starter-data-redis
spring-boot-starter-mail
spring-boot-starter-amqp自定义 Starter
第三方或企业内部自定义的 Starter 遵循 {模块名}-spring-boot-starter 的格式:
mybatis-spring-boot-starter
sms-spring-boot-starter
oss-spring-boot-starter注意:切勿使用
spring-boot-starter-前缀,这会与官方 Starter 产生冲突,并可能引起用户混淆。命名空间隔离是一项重要的生态规则。
项目命名与分组
groupId: com.example.sms
artifactId: sms-spring-boot-starter
version: 1.0.0项目结构
完整 SMS Starter 项目的目录结构如下:
sms-spring-boot-starter
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── sms
│ │ │ ├── SmsAutoConfiguration.java
│ │ │ ├── SmsProperties.java
│ │ │ ├── SmsSender.java
│ │ │ ├── SmsSenderFactory.java
│ │ │ ├── AliyunSmsSender.java
│ │ │ ├── TencentSmsSender.java
│ │ │ └── SmsAutoConfigurationMetadata.java
│ │ └── resources
│ │ └── META-INF
│ │ ├── spring.factories
│ │ ├── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ └── spring-autoconfigure-metadata.properties
│ └── test
│ └── java
│ └── com
│ └── example
│ └── sms
│ └── SmsAutoConfigurationTest.java实战:短信发送 Starter
第一步:创建 Maven 项目
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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.sms</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.0</spring-boot.version>
</properties>
<dependencies>
<!-- Spring Boot 自动配置核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- 可选:配置处理器,生成配置元数据(IDE 提示用) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
<!-- 可选:用于 @ConfigurationProperties 注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
<!-- 阿里云 SMS SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version>
<optional>true</optional>
</dependency>
<!-- 腾讯云 SMS SDK -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<version>3.1.1000</version>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>关键点说明:
- 核心依赖仅
spring-boot-autoconfigure,不依赖spring-boot-starter,因为 Starter 本质是一个普通的自动配置模块- 云厂商 SDK 均标记为
<optional>true</optional>,让用户按需引入spring-boot-configuration-processor编译时生成配置元数据,提升 IDE 体验
第二步:定义配置属性类
使用 @ConfigurationProperties 实现类型安全的属性绑定。
package com.example.sms;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
/**
* 短信配置属性类。
* 通过 prefix = "sms" 绑定 application.yml 中 sms.* 开头的配置。
*/
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
/** 短信提供商:aliyun / tencent */
@NotBlank
private String provider = "aliyun";
/** 阿里云配置 */
private Aliyun aliyun = new Aliyun();
/** 腾讯云配置 */
private Tencent tencent = new Tencent();
// ---------- Getters & Setters ----------
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
public Aliyun getAliyun() {
return aliyun;
}
public void setAliyun(Aliyun aliyun) {
this.aliyun = aliyun;
}
public Tencent getTencent() {
return tencent;
}
public void setTencent(Tencent tencent) {
this.tencent = tencent;
}
// ---------- 内部嵌套配置类 ----------
/**
* 阿里云 SMS 配置
*/
public static class Aliyun {
@NotBlank(message = "阿里云 accessKeyId 不能为空")
private String accessKeyId;
@NotBlank(message = "阿里云 accessKeySecret 不能为空")
private String accessKeySecret;
@NotBlank(message = "阿里云签名名称不能为空")
private String signName;
@Pattern(regexp = "cn-\\w+", message = "地域格式错误,示例: cn-hangzhou")
private String regionId = "cn-hangzhou";
// Getters & Setters
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 getRegionId() { return regionId; }
public void setRegionId(String regionId) { this.regionId = regionId; }
}
/**
* 腾讯云 SMS 配置
*/
public static class Tencent {
@NotBlank
private String secretId;
@NotBlank
private String secretKey;
@NotBlank
private String appId;
@NotBlank
private String signName;
private String region = "ap-guangzhou";
// Getters & Setters
public String getSecretId() { return secretId; }
public void setSecretId(String secretId) { this.secretId = secretId; }
public String getSecretKey() { return secretKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
public String getAppId() { return appId; }
public void setAppId(String appId) { this.appId = appId; }
public String getSignName() { return signName; }
public void setSignName(String signName) { this.signName = signName; }
public String getRegion() { return region; }
public void setRegion(String region) { this.region = region; }
}
}对应的 application.yml 配置示例
sms:
provider: aliyun # 切换为 tencent 则启用腾讯云
aliyun:
access-key-id: LTAI5tXXXXXXXXXXXX
access-key-secret: XXXXXXXXXXXXXXXXXX
sign-name: 我的应用
region-id: cn-hangzhou
tencent:
secret-id: AKIDXXXXXXXXXXXXXXXX
secret-key: XXXXXXXXXXXXXXXXXX
app-id: 1400XXXXXX
sign-name: 我的应用
region: ap-guangzhou第三步:定义发送接口
package com.example.sms;
/**
* 短信发送统一接口,屏蔽不同云厂商的 API 差异。
*/
public interface SmsSender {
/**
* 发送短信。
*
* @param phone 手机号码
* @param templateCode 短信模板编码
* @param params 模板变量参数(如验证码等)
* @return 发送结果
*/
SmsResult send(String phone, String templateCode, String... params);
}发送结果封装
package com.example.sms;
/**
* 短信发送结果
*/
public class SmsResult {
private boolean success;
private String message;
private String requestId;
public static SmsResult ok(String requestId) {
SmsResult result = new SmsResult();
result.success = true;
result.requestId = requestId;
return result;
}
public static SmsResult fail(String message) {
SmsResult result = new SmsResult();
result.success = false;
result.message = message;
return result;
}
// Getters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getRequestId() { return requestId; }
public void setRequestId(String requestId) { this.requestId = requestId; }
@Override
public String toString() {
return "SmsResult{" +
"success=" + success +
", message='" + message + '\'' +
", requestId='" + requestId + '\'' +
'}';
}
}第四步:实现阿里云 SMS 发送器
package com.example.sms;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
/**
* 阿里云短信发送实现。
* 只有在 classpath 中存在阿里云 SDK 时才会加载。
*/
public class AliyunSmsSender implements SmsSender {
private final Client client;
private final String signName;
public AliyunSmsSender(SmsProperties.Aliyun properties) {
this.signName = properties.getSignName();
try {
Config config = new Config()
.setAccessKeyId(properties.getAccessKeyId())
.setAccessKeySecret(properties.getAccessKeySecret())
.setEndpoint("dysmsapi.aliyuncs.com");
this.client = new Client(config);
} catch (Exception e) {
throw new RuntimeException("初始化阿里云 SMS Client 失败", e);
}
}
@Override
public SmsResult send(String phone, String templateCode, String... params) {
try {
// 将模板参数拼接为 JSON 字符串
String paramJson = buildParamJson(params);
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(signName)
.setTemplateCode(templateCode)
.setTemplateParam(paramJson);
SendSmsResponse response = client.sendSms(request);
String code = response.getBody().getCode();
if ("OK".equals(code)) {
return SmsResult.ok(response.getBody().getRequestId());
}
return SmsResult.fail(response.getBody().getMessage());
} catch (Exception e) {
return SmsResult.fail("发送异常: " + e.getMessage());
}
}
private String buildParamJson(String... params) {
StringBuilder sb = new StringBuilder("{");
for (int i = 0; i < params.length; i++) {
if (i > 0) sb.append(",");
sb.append("\"param").append(i + 1).append("\":\"")
.append(params[i]).append("\"");
}
sb.append("}");
return sb.toString();
}
}第五步:实现腾讯云 SMS 发送器
package com.example.sms;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
/**
* 腾讯云短信发送实现。
* 只有在 classpath 中存在腾讯云 SDK 时才会加载。
*/
public class TencentSmsSender implements SmsSender {
private final SmsClient client;
private final String appId;
private final String signName;
public TencentSmsSender(SmsProperties.Tencent properties) {
this.appId = properties.getAppId();
this.signName = properties.getSignName();
Credential credential = new Credential(
properties.getSecretId(),
properties.getSecretKey()
);
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint("sms.tencentcloudapi.com");
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
this.client = new SmsClient(credential, properties.getRegion(), clientProfile);
}
@Override
public SmsResult send(String phone, String templateCode, String... params) {
try {
SendSmsRequest request = new SendSmsRequest();
request.setSmsSdkAppId(appId);
request.setSignName(signName);
request.setTemplateId(templateCode);
request.setPhoneNumberSet(new String[]{"+86" + phone});
request.setTemplateParamSet(params);
SendSmsResponse response = client.SendSms(request);
String status = response.getSendStatusSet()[0].getCode();
if ("Ok".equalsIgnoreCase(status)) {
return SmsResult.ok(response.getSendStatusSet()[0].getSerialNo());
}
return SmsResult.fail(response.getSendStatusSet()[0].getMessage());
} catch (Exception e) {
return SmsResult.fail("发送异常: " + e.getMessage());
}
}
}第六步:自动配置类
自动配置类是 Starter 的核心。它负责判断条件是否满足、装配 Bean。
package com.example.sms;
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;
/**
* 短信发送自动配置类。
*
* 设计要点:
* 1. 使用 @AutoConfiguration 替代 Spring Boot 3.x 之前的 @Configuration
* 2. 通过 @ConditionalOnClass 判断 SDK 是否在 classpath 中
* 3. 通过 @ConditionalOnProperty 控制阿里云/腾讯云切换
* 4. 通过 @ConditionalOnMissingBean 允许用户覆盖默认 Bean
*/
@AutoConfiguration
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {
/**
* 阿里云 SMS 发送器。
* 条件:
* - classpath 中存在阿里云 Client 类
* - 配置 sms.provider=aliyun(默认值)
* - 尚未定义 SmsSender Bean
*/
@Bean
@ConditionalOnClass(name = "com.aliyun.dysmsapi20170525.Client")
@ConditionalOnProperty(prefix = "sms", name = "provider", havingValue = "aliyun", matchIfMissing = true)
@ConditionalOnMissingBean(SmsSender.class)
public SmsSender aliyunSmsSender(SmsProperties properties) {
return new AliyunSmsSender(properties.getAliyun());
}
/**
* 腾讯云 SMS 发送器。
* 条件:
* - classpath 中存在腾讯云 SmsClient 类
* - 配置 sms.provider=tencent
* - 尚未定义 SmsSender Bean
*/
@Bean
@ConditionalOnClass(name = "com.tencentcloudapi.sms.v20210111.SmsClient")
@ConditionalOnProperty(prefix = "sms", name = "provider", havingValue = "tencent")
@ConditionalOnMissingBean(SmsSender.class)
public SmsSender tencentSmsSender(SmsProperties properties) {
return new TencentSmsSender(properties.getTencent());
}
}条件注解设计解读
| 注解 | 作用 |
|---|---|
@AutoConfiguration | 声明此类为自动配置类,Spring Boot 3.x 推荐替代 @Configuration |
@ConditionalOnClass | 仅在 classpath 中存在指定类时加载该 Bean,实现按需装配 |
@ConditionalOnProperty | 根据 application.yml 中的配置值决定是否创建 Bean |
@ConditionalOnMissingBean | 只有当容器中不存在 SmsSender Bean 时才会创建,允许用户自定义覆盖 |
@EnableConfigurationProperties | 启用配置属性绑定,将 SmsProperties 注册到 Spring 容器 |
切换逻辑说明
- 阿里云方案:将
sms.provider设置为aliyun(默认值)并在 classpath 中包含阿里云 SDK - 腾讯云方案:将
sms.provider设置为tencent并在 classpath 中包含腾讯云 SDK - 自定义方案:用户自行实现
SmsSender接口并声明为@Bean,自动配置自动失效
在 Spring Boot 3.x 中使用 @AutoConfiguration
// Spring Boot 3.x / Spring Framework 6.x 开始,@AutoConfiguration 是推荐做法
// 它和 @Configuration 的区别在于:@AutoConfiguration 专门用于自动配置,
// 允许与 AutoConfiguration.imports 文件配合,且加载时机在用户 @Configuration 之后。
import org.springframework.boot.autoconfigure.AutoConfiguration;
@AutoConfiguration
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {
// ...
}第七步:注册自动配置
Spring Boot 提供了两种注册自动配置类的机制。
方式一:AutoConfiguration.imports(Spring Boot 2.7+ / 3.x 推荐)
在 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中:
com.example.sms.SmsAutoConfiguration这是 Spring Boot 2.7 引入的新机制,替代了传统的 spring.factories。Spring Boot 3.x 已完全移除对 spring.factories 中自动配置注册的支持。
方式二:spring.factories(Spring Boot 2.x 兼容)
在 src/main/resources/META-INF/spring.factories 文件中:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sms.SmsAutoConfiguration兼容性建议:如果项目需要同时兼容 Spring Boot 2.x 和 3.x,可以同时保留两个文件。Spring Boot 2.x 读取
spring.factories,Spring Boot 3.x 读取AutoConfiguration.imports。
两种机制的演进历史
Spring Boot 1.x ──── spring.factories(唯一方式)
Spring Boot 2.0-2.6 ─ spring.factories
Spring Boot 2.7+ ─ spring.factories + AutoConfiguration.imports(并存期)
Spring Boot 3.x ─ AutoConfiguration.imports(唯一方式,spring.factories 被移除)第八步:自动配置元数据优化
spring-autoconfigure-metadata.properties 文件可以过滤不必要的自动配置加载,显著提升应用启动速度。
在 src/main/resources/META-INF/spring-autoconfigure-metadata.properties 中:
# 自动配置类全限定名
com.example.sms.SmsAutoConfiguration=
#
# 配置条件过滤:仅当 sms.provider 属性存在时,才尝试解析此自动配置
com.example.sms.SmsAutoConfiguration.ConditionalOnProperty=sms.provider
#
# 类条件过滤:仅当这些类存在于 classpath 中,才尝试解析此自动配置
com.example.sms.SmsAutoConfiguration.ConditionalOnClass=com.aliyun.dysmsapi20170525.Client,com.tencentcloudapi.sms.v20210111.SmsClient为什么需要自动配置元数据?
Spring Boot 在启动时会对所有 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册的自动配置类进行条件评估。如果不加过滤,每次启动都要加载并解析这些类的条件注解,造成不必要的类加载和反射开销。
配置元数据允许 Spring Boot 在类加载之前就基于简单的条件表达式判断是否需要处理该类——这属于"提前过滤"阶段,效率远高于完整的条件注解解析。
测试数据显示,在一个包含 50+ 自动配置模块的大型项目中,合理的元数据配置可以将启动时间缩短 30%~50%。
元数据配置项参考
# 自动配置的基本信息
com.example.sms.SmsAutoConfiguration=
# 配置条件(ConditionalOnProperty)
com.example.sms.SmsAutoConfiguration.ConditionalOnProperty=sms.provider
# 类条件(ConditionalOnClass),多个类用逗号分隔
com.example.sms.SmsAutoConfiguration.ConditionalOnClass=com.aliyun.dysmsapi20170525.Client,com.tencentcloudapi.sms.v20210111.SmsClient
# Bean 条件(ConditionalOnBean)
# 如果自动配置依赖于某些特定的 Bean 存在才能生效
# com.example.sms.SmsAutoConfiguration.ConditionalOnBean=some.RequiredBean
# Resource 条件(ConditionalOnResource)
# com.example.sms.SmsAutoConfiguration.ConditionalOnResource=classpath:sms-config.xml第九步:自动配置的测试方法
单元测试:使用测试切片
package com.example.sms;
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;
/**
* 短信自动配置测试。
* 使用 ApplicationContextRunner 模拟 Spring 容器,无需启动完整应用。
*/
class SmsAutoConfigurationTest {
/**
* 测试默认配置:provider=aliyun,应自动装配 AliyunSmsSender。
*/
private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SmsAutoConfiguration.class));
@Test
void shouldCreateAliyunSmsSenderByDefault() {
this.contextRunner
.withPropertyValues(
"sms.aliyun.access-key-id=test-key",
"sms.aliyun.access-key-secret=test-secret",
"sms.aliyun.sign-name=test-sign"
)
.run(context -> {
// 验证 SmsSender 被创建
assertThat(context).hasSingleBean(SmsSender.class);
// SmsSender 实现类应为 AliyunSmsSender
SmsSender sender = context.getBean(SmsSender.class);
assertThat(sender).isInstanceOf(AliyunSmsSender.class);
// Properties 已正确绑定
SmsProperties properties = context.getBean(SmsProperties.class);
assertThat(properties.getProvider()).isEqualTo("aliyun");
assertThat(properties.getAliyun().getAccessKeyId()).isEqualTo("test-key");
});
}
@Test
void shouldRespectUserDefinedSmsSender() {
this.contextRunner
.withBean("customSmsSender", SmsSender.class, () -> (phone, templateCode, params) -> {
return SmsResult.ok("custom-req-id");
})
.run(context -> {
// 用户自定义 Bean 应覆盖自动配置
assertThat(context).hasSingleBean(SmsSender.class);
SmsSender sender = context.getBean(SmsSender.class);
SmsResult result = sender.send("13800138000", "TMP001", "123456");
assertThat(result.isSuccess()).isTrue();
assertThat(result.getRequestId()).isEqualTo("custom-req-id");
});
}
@Test
void shouldNotCreateSmsSenderWhenPropertyMismatches() {
this.contextRunner
.withPropertyValues(
"sms.aliyun.access-key-id=test-key",
"sms.aliyun.access-key-secret=test-secret",
"sms.aliyun.sign-name=test-sign",
"sms.provider=nonexistent"
)
.run(context -> {
// provider 既不是 aliyun 也不是 tencent,不应有 SmsSender
assertThat(context).doesNotHaveBean(SmsSender.class);
});
}
}集成测试:在完整应用中验证
package com.example.sms;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 集成测试,需要引入 spring-boot-starter-test 并在应用中启用自动配置。
* 建议配合测试 profile 使用 mock 的 SDK 连接测试账号。
*/
@SpringBootTest
@TestPropertySource(properties = {
"sms.provider=aliyun",
"sms.aliyun.access-key-id=test",
"sms.aliyun.access-key-secret=test",
"sms.aliyun.sign-name=test-sign"
})
class SmsAutoConfigurationIntegrationTest {
@Autowired
private SmsSender smsSender;
@Autowired
private SmsProperties smsProperties;
@Test
void contextLoads() {
assertThat(smsSender).isNotNull();
assertThat(smsProperties).isNotNull();
assertThat(smsProperties.getProvider()).isEqualTo("aliyun");
}
@Test
void smsSenderShouldBeAliyunInstance() {
assertThat(smsSender).isInstanceOf(AliyunSmsSender.class);
}
}测试的关键点和最佳实践
- 使用
ApplicationContextRunner:轻量级测试工具,避免启动完整 Spring Boot 应用,适合对自动配置类进行隔离测试 - 覆盖三种核心场景:
- 正常加载场景(验证 Bean 创建和属性绑定)
- 用户覆盖场景(验证
@ConditionalOnMissingBean生效) - 条件不满足场景(验证不会创建不应存在的 Bean)
- Mock 外部依赖:测试短信发送器时,建议 Mock SDK Client 或使用 WireMock 模拟 HTTP 调用,避免实际调用云厂商 API
第十步:发布到 Maven 仓库的注意事项
本地安装验证
在发布之前,先在本地安装验证 Starter 能否正常工作:
# 安装到本地 Maven 仓库
mvn clean install
# 在其他项目中引入测试POM 配置最佳实践
<!-- 发布到 Maven 仓库时,以下配置至关重要 -->
<distributionManagement>
<repository>
<!-- 正式版仓库 -->
<id>releases</id>
<url>https://your-nexus/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<!-- 快照版仓库 -->
<id>snapshots</id>
<url>https://your-nexus/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
<build>
<plugins>
<!-- 源码插件:发布源码包,方便使用者调试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<!-- Javadoc 插件:发布文档包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<id>attach-javadoc</id>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<!-- 额外依赖:确保配置处理器在编译时生效 -->
<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>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>发布清单检查
以下是发布到 Maven 仓库前的核心检查事项:
| 检查项 | 说明 | 重要性 |
|---|---|---|
| 命名规范 | 使用 xxx-spring-boot-starter 格式 | ❗必须 |
| 版本管理 | 明确区分 SNAPSHOT 和 RELEASE 版本 | ❗必须 |
| 依赖范围 | 可选依赖必须标记 <optional>true</optional> | ❗必须 |
| 认证配置 | 在 settings.xml 中配置仓库认证信息 | ❗必须 |
| 配置元数据 | 生成 spring-configuration-metadata.json | ⭐推荐 |
| 自动配置元数据 | 配置 spring-autoconfigure-metadata.properties | ⭐推荐 |
| 源码包 | 发布源码 JAR 方便调试 | ⭐推荐 |
| 文档包 | 发布 Javadoc JAR | ⭐推荐 |
| 许可证 | 明确项目许可证信息 | ⭐推荐 |
| 跨版本兼容 | 如需兼容 Spring Boot 2.x,保留 spring.factories | ⭐推荐 |
依赖管理策略
<!-- 使用方如何引入 Starter -->
<dependency>
<groupId>com.example.sms</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 根据选用的云厂商,按需引入 SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version>
</dependency>版本管理建议
版本号规范:
- 主版本号:不兼容的大版本变更
- 次版本号:向下兼容的功能新增
- 修订号:向下兼容的 Bug 修复
与 Spring Boot 版本匹配建议:
sms-spring-boot-starter: 1.0.x → Spring Boot 2.x
sms-spring-boot-starter: 2.0.x → Spring Boot 3.x完整配置示例
用户在使用 Starter 时,只需在最简配置下即可运行:
application.yml 最简配置(阿里云)
sms:
provider: aliyun
aliyun:
access-key-id: ${SMS_ALIYUN_ACCESS_KEY_ID}
access-key-secret: ${SMS_ALIYUN_ACCESS_KEY_SECRET}
sign-name: ${SMS_ALIYUN_SIGN_NAME}application.yml 最简配置(腾讯云)
sms:
provider: tencent
tencent:
secret-id: ${SMS_TENCENT_SECRET_ID}
secret-key: ${SMS_TENCENT_SECRET_KEY}
app-id: ${SMS_TENCENT_APP_ID}
sign-name: ${SMS_TENCENT_SIGN_NAME}业务代码中使用
import com.example.sms.SmsSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class VerificationCodeService {
@Autowired
private SmsSender smsSender;
public void sendCode(String phone, String code) {
SmsResult result = smsSender.send(
phone,
"SMS_XXXXXXX", // 模板编码
code // 模板变量
);
if (!result.isSuccess()) {
throw new RuntimeException("短信发送失败: " + result.getMessage());
}
}
}自动配置的核心设计模式总结
模式一:条件自动配置
@AutoConfiguration
+ @ConditionalOnClass → 按 classpath 中的类决定是否加载
+ @ConditionalOnProperty → 按配置属性决定是否加载
+ @ConditionalOnMissingBean → 允许用户覆盖默认实现模式二:类型安全配置
@ConfigurationProperties(prefix = "sms")
+ @EnableConfigurationProperties
→ 将 application.yml 中的属性自动映射到 POJO,支持 IDE 提示和校验模式三:SPI 式加载
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
→ Spring Boot 通过此文件发现自动配置类(SPI 机制)
→ 加载后评估条件注解,决定哪些配置生效自动配置加载流程
应用启动
│
▼
读取 META-INF/spring/.../AutoConfiguration.imports
│
▼
加载 spring-autoconfigure-metadata.properties(提前过滤)
│
▼
筛选出候选自动配置类
│
▼
解析 @ConditionalOnClass(类加载条件)
│
▼
解析 @ConditionalOnBean / @ConditionalOnMissingBean
│
▼
解析 @ConditionalOnProperty / @ConditionalOnResource 等
│
▼
创建符合条件的 Bean → 完成自动配置常见问题与排查
自动配置未生效
检查步骤:
1. 确认 META-INF 下的注册文件位置正确
2. 确认条件注解的条件已满足(类存在、属性匹配等)
3. 开启 debug 日志:logging.level.org.springframework.boot.autoconfigure=DEBUG
4. 查看 Positive matches(已匹配)和 Negative matches(未匹配)输出配置属性 IDE 无提示
原因:缺少 spring-configuration-metadata.json
解决:确保 spring-boot-configuration-processor 依赖已添加且正确配置Bean 冲突
场景:用户自定义的 SmsSender 与实际加载的自动配置冲突
解决:@ConditionalOnMissingBean 设计可避免此问题;
如果仍有冲突,可通过 @Primary 或 @Qualifier 指定总结
本文从零构建了一个短信发送 Starter,完整覆盖了 Spring Boot 自动配置的核心设计模式。通过这个实战案例,可以提炼出开发 Starter 的几个核心原则:
- 最小依赖原则:Starter 本身只依赖
spring-boot-autoconfigure,具体的 SDK 实现均作为可选依赖 - 条件装配原则:充分利用
@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean等条件注解 - 开箱即用原则:通过合理的默认值设计,让用户在最少的配置下即可使用
- 可扩展原则:通过接口抽象和
@ConditionalOnMissingBean设计,允许用户替换默认实现
遵循这些原则开发的自定义 Starter,既能在团队内部高效复用,也适合作为开源项目发布到 Maven 中央仓库供社区使用。