从一个重复配置说起
做后台项目时,很多人会遇到这样的场景:三个服务都要发短信、都要统一包装第三方接口的签名逻辑,于是每个项目里都复制一份配置类和工具类。后面换密钥、改超时,得挨个改;某个项目漏改,线上就开始报鉴权失败。
Spring Boot 的 starter 就是为这种"重复配置"准备的。它不是框架作者才能写的东西,业务团队把一组常用配置和依赖打包成 starter,别的项目引一个依赖、写两行配置就能用。这篇文章从条件注解讲到自定义 starter,最后给出一个可以直接套用的模板。
条件注解不止三个
写 starter 的核心不是写代码,而是写"什么时候才装配"的判断。常用的条件注解有一组:
| 注解 | 成立条件 |
|---|---|
@ConditionalOnClass |
类路径上有指定类 |
@ConditionalOnMissingClass |
类路径上没有指定类 |
@ConditionalOnBean |
容器里已有指定 Bean |
@ConditionalOnMissingBean |
容器里没有指定 Bean |
@ConditionalOnProperty |
配置项存在且值匹配 |
@ConditionalOnWebApplication |
是 Web 应用 |
@ConditionalOnExpression |
SpEL 表达式为真 |
组合使用时要注意判断顺序:@ConditionalOnClass 在类加载阶段就能判断,适合用来"有某个依赖才启用";@ConditionalOnMissingBean 必须在 Bean 定义阶段判断,适合用来"用户没配才用默认值"。这两个注解经常放在同一个自动配置类上。
写一个最小 Starter
一个可维护的 starter 通常拆成两个模块:demo-spring-boot-autoconfigure 放自动配置和属性类,demo-spring-boot-starter 只放依赖聚合。小团队也可以合成一个模块,但不要把自己的自动配置类放在业务包下面。
先定义属性类:
@ConfigurationProperties(prefix = "demo.sms")
public class SmsProperties {
private boolean enabled = false;
private String endpoint = "https://sms.example.com";
private int timeoutSeconds = 3;
// getter / setter 省略
}
再写自动配置类:
@AutoConfiguration
@ConditionalOnClass(SmsClient.class)
@ConditionalOnProperty(prefix = "demo.sms", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SmsClient smsClient(SmsProperties properties) {
return new SmsClient(properties.getEndpoint(), properties.getTimeoutSeconds());
}
}
最后在 src/main/resources/META-INF/spring/ 下新建 org.springframework.boot.autoconfigure.AutoConfiguration.imports,内容只有一行:
com.example.sms.autoconfigure.SmsAutoConfiguration
如果是 Spring Boot 2.7 之前的项目,这个文件对应的是 META-INF/spring.factories 里的 org.springframework.boot.autoconfigure.EnableAutoConfiguration=... 配置。2.7 两种都支持,3.0 只认 .imports,新项目直接按新写法来。
怎么验证它真的生效
不要写完就扔进业务项目里试,先写一个上下文测试:
class SmsAutoConfigurationTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SmsAutoConfiguration.class));
@Test
void shouldNotCreateClientWhenDisabled() {
runner.run(context -> assertThat(context).doesNotHaveBean(SmsClient.class));
}
@Test
void shouldCreateClientWhenEnabled() {
runner.withPropertyValues("demo.sms.enabled=true")
.run(context -> assertThat(context).hasSingleBean(SmsClient.class));
}
}
ApplicationContextRunner 只加载指定的自动配置类,启动快,很适合验证条件是否按预期成立。接入真实项目后,再用 --debug 看 Positive matches 里有没有你的自动配置类,或者访问 /actuator/conditions 确认。
常见坑
1. imports 文件路径或文件名写错
这个文件名很长,少一个 spring 目录、把 AutoConfiguration 拼错,Spring 都不会报错,只会安静地不装配。排查时先确认文件在编译后的 target/classes/META-INF/spring/ 下存在。
2. 自动配置类被组件扫描扫到
自动配置类一旦被 @ComponentScan 扫到,就会变成普通配置类,条件注解的语义可能和预期不一致,还可能出现重复注册。把它放在业务启动类扫描范围之外的包,是更稳妥的做法。
3. starter 里写 @ComponentScan
starter 是给别人用的,扫描范围应该由使用方决定。在 starter 里写 @ComponentScan("com.example") 很容易扫到使用方的同名包,造成难以定位的冲突。
4. @ConditionalOnMissingBean 放错位置
放在 @Bean 方法上只对这一个 Bean 生效,放在类上则表示"整个配置类里没有同类型 Bean 才装配"。如果类里有多个 @Bean,要逐个确认语义。
5. 依赖传递把使用方拖下水
starter 的 pom 里不要引入一堆业务依赖,能 optional 的就 optional。否则使用方只是想要一个短信客户端,却被顺带升级了 Jackson、Netty 的版本。
6. 升级 Boot 3 后没有迁移
除了 .imports 文件,还要检查 javax.* 是否已经换成 jakarta.*。很多内部 starter 在 2023 年做 3.0 适配时,就是卡在这两件事上。
小结
自定义 starter 的骨架是:属性类负责接配置,自动配置类负责按条件创建 Bean,.imports 文件负责告诉 Spring 去哪儿找。把条件注解写清楚、把默认 Bean 标上 @ConditionalOnMissingBean,使用方就能"引依赖即用,需要时再覆盖"。
评论