当项目发展到一定规模,团队内部会积累一批通用的技术组件(如公司统一的消息工具、加解密库、监控埋点等)。把每一个组件都写成一个独立的 Starter,可以让所有 Spring Boot 工程通过引入一个依赖就能自动获得这些能力,真正做到“一处开发,多处复用”。本节将一步一步展示一个规范的自定义 Starter 从零到可用的完整过程。
17.1.1 Starter 的本质与命名约定
Starter 本身不包含业务逻辑,它的职责是完成两件事:
- 把你需要复用的库(jar)带进来(通过 Maven/Gradle 依赖);
- 提供自动配置类,将该库的核心 Bean 自动注册到 Spring 容器。
一个标准的 Starter 通常包含两个模块(虽然简单场景可以合二为一)
xxx-spring-boot-autoconfigure:包含自动配置类、属性映射类、配置元数据等。xxx-spring-boot-starter:一个近乎空的模块,只依赖上面的 autoconfigure 模块以及功能所需的第三方库。它存在的意义是让使用方只需引入一个 GAV 坐标,无需关心内部的拆解。
官方推荐的命名规范:
- 官方 Starter:
spring-boot-starter-{功能},如spring-boot-starter-data-redis - 自定义 Starter:
{功能}-spring-boot-starter,如mycompany-log-spring-boot-starter
17.1.2 一个实用案例:短信服务 Starter
假设我们要封装一个短信发送服务,对接公司内部的 SMS 网关。目标让业务模块只需引入 sms-spring-boot-starter,并在配置文件中写几个属性,就能在代码中直接注入 SmsClient 使用。
最终效果:
# application.yml
sms:
enabled: true
endpoint: https://sms-api.example.com
app-id: myApp
secret-key: ${SMS_SECRET}
@Service
public class VerificationService {
private final SmsClient smsClient;
public VerificationService(SmsClient smsClient) {
this.smsClient = smsClient;
}
public void sendCode(String phone, String code) {
smsClient.send(phone, "您的验证码是:" + code);
}
}
17.1.3 开发 autoconfigure 模块
1. 创建项目结构
sms-spring-boot-autoconfigure
├── pom.xml
└── src/main/java/com/example/sms/
├── SmsProperties.java
├── SmsClient.java
└── autoconfigure/
└── SmsAutoConfiguration.java
2. 定义属性类
使用 @ConfigurationProperties 将配置项映射为类型安全的 Java 对象,同时利用 @Validated 做基本校验。
package com.example.sms;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotBlank;
@Validated
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
private boolean enabled = true;
@NotBlank(message = "SMS endpoint is required")
private String endpoint;
@NotBlank(message = "SMS app-id is required")
private String appId;
@NotBlank(message = "SMS secret-key is required")
private String secretKey;
// getters and setters...
}
属性类中的 enabled 字段用于提供一个“开关”,可以在配置中关闭整个服务。
3. 编写功能组件
SmsClient 就是真正提供短信发送能力的类,它的初始化依赖上述属性。
package com.example.sms;
public class SmsClient {
private final SmsProperties properties;
public SmsClient(SmsProperties properties) {
this.properties = properties;
}
public void send(String phone, String message) {
if (!properties.isEnabled()) {
throw new IllegalStateException("SMS client is disabled");
}
// 实际调用短信网关
System.out.printf("Sending to %s: %s (via %s)\n",
phone, message, properties.getEndpoint());
}
}
4. 编写自动配置类
这是 Starter 的大脑,决定哪些 Bean 在什么条件下被创建。
package com.example.sms.autoconfigure;
import com.example.sms.SmsClient;
import com.example.sms.SmsProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(SmsProperties.class)
@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true", matchIfMissing = true)
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SmsClient smsClient(SmsProperties properties) {
return new SmsClient(properties);
}
}
关键注解解读:
@EnableConfigurationProperties:激活SmsProperties,使其可以被注入。@ConditionalOnProperty:当sms.enabled=true时(或未配置时,默认 true),配置类生效;否则整个SmsClient不会被创建。@ConditionalOnMissingBean:如果容器中已存在用户自定义的SmsClient,则不再创建默认的,尊重使用方的覆盖。
5. 注册自动配置类
Spring Boot 2.7 之前,需要在 META-INF/spring.factories 中声明:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sms.autoconfigure.SmsAutoConfiguration
从 Spring Boot 2.7 开始,推荐使用新的方式:在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中直接写入自动配置类的全限定名,每行一个。
com.example.sms.autoconfigure.SmsAutoConfiguration
如果使用较新版本,建议优先使用 .imports 文件。
6. 提供配置元数据(可选但推荐)
为了让 IDE 能够自动提示配置项,可以在 META-INF/spring-configuration-metadata.json 或通过添加 spring-boot-configuration-processor 依赖自动生成。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
编译后在 META-INF 下会生成 additional-spring-configuration-metadata.json 和标准元数据文件,IDE 即可识别 sms 前缀下的所有属性。
17.1.4 创建 Starter 模块
sms-spring-boot-starter 模块极其简单,pom.xml 中只声明对 autoconfigure 模块的依赖(以及 SmsClient 运行时需要的第三方库)。这样,使用方只需引入这个 Starter:
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
内部 pom 大致为:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<artifactId>sms-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>
<!-- 若 SmsClient 依赖其他库(如 HTTP 客户端),也在此引入 -->
</dependencies>
至此,一个规范的 Starter 开发完成。将两个模块安装或发布到公司私有仓库,其他项目即可直接使用。
17.1.5 测试 Starter 的完整性
在发布前,我们应编写一个简单的集成测试来验证 Starter 的行为。
在 autoconfigure 模块的 src/test 中,使用 ApplicationContextRunner 可以模拟应用启动并断言 Bean 的存在情况,无需完整的 Spring Boot 测试上下文。
class SmsAutoConfigurationTest {
private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SmsAutoConfiguration.class));
@Test
void shouldCreateSmsClientWhenPropertyEnabled() {
contextRunner
.withPropertyValues(
"sms.endpoint=https://test.com",
"sms.app-id=test",
"sms.secret-key=secret")
.run(ctx -> {
assertThat(ctx).hasSingleBean(SmsClient.class);
assertThat(ctx).hasSingleBean(SmsProperties.class);
});
}
@Test
void shouldNotCreateSmsClientWhenPropertyDisabled() {
contextRunner
.withPropertyValues(
"sms.enabled=false",
"sms.endpoint=https://test.com",
"sms.app-id=test",
"sms.secret-key=secret")
.run(ctx -> assertThat(ctx).doesNotHaveBean(SmsClient.class));
}
@Test
void shouldUseUserDefinedClientWhenPresent() {
SmsClient customClient = new SmsClient(null);
contextRunner
.withPropertyValues(
"sms.endpoint=https://test.com",
"sms.app-id=test",
"sms.secret-key=secret")
.withBean(SmsClient.class, () -> customClient)
.run(ctx -> assertThat(ctx.getBean(SmsClient.class)).isSameAs(customClient));
}
}
这些测试确保自动配置在正常、关闭和用户自定义 Bean 的场景下都符合预期。
17.1.6 开发规范与最佳实践
- 属性前缀隔离:使用自定义前缀(如
sms)避免与企业级配置冲突。 - 提供合理的默认值:属性类的字段尽可能有默认值(如
enabled=true),降低使用门槛。 - 通过
@Conditional精细控制:善用@ConditionalOnClass(确保某个类在 classpath 下才生效)、@ConditionalOnMissingBean(允许用户覆盖)、@ConditionalOnWebApplication(区分 Web 与非 Web 环境)等条件注解。 - 对外暴露的类尽量保持稳定:自动配置类和属性类是 Starter 的公开 API,修改时要注意兼容性。
- 发布前进行严格的依赖收敛:Starter 传递进来的第三方库应与主项目兼容,避免版本冲突。
- 提供 starter 自身的监控信息:如果涉及连接池、线程池等资源,考虑通过 Actuator 暴露指标。
17.1.7 实战小结
从属性类到自动配置类,从条件注解到 Spring Boot 的注册机制,一个高质量 Starter 背后体现的是 约定优于配置 和 封闭可扩展 的设计原则。掌握它的开发流程,意味着你可以将公司内部的所有“技术积木”标准化、模块化,让团队里的每一个 Spring Boot 项目都能以极低的成本获得最强基础设施能力,真正实现技术资产的复用与传承。