ElIauk

Spring Boot 条件注解与自定义 Starter

从一个重复配置说起 做后台项目时,很多人会遇到这样的场景:三个服务都要发短信、都要统一包装第三方接口的签名逻辑,于是每个项目里都复制一份配置类和工具类。后面换密钥、改超时,得挨个改;某个项目漏改,线上就开始报鉴权失败。 Spring Boot 的 starter 就是为这种"重复配置"准备的。它不是

评论
Spring Boot 条件注解与自定义 Starter

从一个重复配置说起

做后台项目时,很多人会遇到这样的场景:三个服务都要发短信、都要统一包装第三方接口的签名逻辑,于是每个项目里都复制一份配置类和工具类。后面换密钥、改超时,得挨个改;某个项目漏改,线上就开始报鉴权失败。

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,使用方就能"引依赖即用,需要时再覆盖"。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者