SpringBoot的自动配置到底是怎么跑起来的?很多人用了好久@SpringBootApplication,天天喊“约定大于配置”,但真到了要自己封装一个公共组件、给团队或者多个项目复用的时候,从哪下手都不知道。今天这篇就动手写一个自己的Starter,把@Conditional、AutoConfiguration.imports、属性绑定、SPI机制这些平时只闻其名不见其身的核心原理,全部掰开揉碎走一遍。文章适合已经能熟练使用SpringBoot,但还没读过自动配置源码、没自己封装过组件的开发者。
先说清楚,看完这篇文章你能带走两样东西:第一,对SpringBoot自动配置机制的完整理解链路,知道一个Starter在启动时经历哪几个阶段;第二,一个可以直接放在公司内部项目里使用的短信发送Starter完整代码骨架,包含属性配置、自动装配、条件判断和数据校验,照着抄就能用。
1. Starter的本质与自动配置链路拆解
1.1 一个Starter在SpringBoot里到底是什么
Starter不是一个新的框架,也不是一个特殊的包。它本质上就是一个遵循了特定约定的普通Maven/Gradle模块,里面放了一组自动配置类,再加上若干可选依赖。SpringBoot在启动时会扫描所有依赖jar包里的自动配置声明文件,把符合条件的配置类加载进容器,完成Bean的注册。
我见过不少人第一次看spring-boot-autoconfigure的源码时被吓到,几千个类、几百个配置类,感觉无从下手。其实不用怕,Starter的机制很简单,只需要抓住三个核心文件:
- 自动配置类:用
@AutoConfiguration标注的类,里面通过@Bean声明要装配的组件。 - 配置声明文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,老版本是spring.factories,告诉SpringBoot“我这个jar里有这些自动配置类”。 - 属性绑定类:用
@ConfigurationProperties标注的数据类,负责把application.yml里的配置项映射成Java对象。
这三个文件加起来,就是一个Starter的完整骨架。SpringBoot启动时,通过SPI机制读取所有jar包里的配置声明文件,再对每个自动配置类做条件判断,满足条件的就执行装配逻辑。
1.2 从启动到Bean装配,中间经历了什么
为了真正理解手写Starter时需要关注哪些点,必须把SpringBoot启动时的装配链路走一遍。我把关键步骤总结成下面这张流程表,建议对着源码SpringApplication.run()到AutoConfigurationImportSelector走一遍:
| 阶段 | 具体动作 | 关键类/注解 |
|---|---|---|
| 1. 配置类解析 | 解析启动类上的@SpringBootApplication,找到@EnableAutoConfiguration元注解 | SpringApplication / AnnotatedBeanDefinitionReader |
| 2. SPI加载 | 通过类加载器读取所有依赖jar包中的自动配置声明文件 | AutoConfigurationImportSelector / SpringFactoriesLoader |
| 3. 排序去重 | 对候选配置类按@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder排序 | AutoConfigurationSorter |
| 4. 条件过滤 | 逐个执行配置类上的@Conditional系列注解,不满足条件的直接跳过 | ConditionEvaluator / OnClassCondition 等 |
| 5. Bean注册 | 将通过的配置类作为@Configuration处理,执行里面的@Bean方法 | ConfigurationClassPostProcessor |
| 6. 属性绑定 | 将配置文件中对应前缀的属性绑定到@ConfigurationProperties类 | ConfigurationPropertiesBindingPostProcessor |
很多人卡在第四步“条件过滤”上,总觉得@ConditionalOnClass、@ConditionalOnMissingBean是个玄学。其实可以打个比方:自动配置类就像公司里的备用方案,平时不生效,只有当“某个类在classpath里存在”或者“容器里没有指定Bean”这些条件满足时,备用方案才真正启动。坐高铁检票进站的时候,人脸识别闸机就是“自动配置”——你已经买票了(依赖存在)而且没有走人工通道(没有自定义Bean),闸机就自动放行。条件注解就是闸机上的判断逻辑。
2. 手写Starter的前期准备与方案设计
2.1 业务场景:做一个实用的短信发送Starter
空谈原理没意思,得有一个具体场景。这次我选短信发送这个业务来写Starter。为什么选短信?因为它在团队开发中太常用了:验证码登录、营销通知、报警提醒,到处都是短信。更关键的是,短信发送一般都要对接第三方服务商,而不同项目对接的服务商可能不同,有的用阿里云、有的用腾讯云、有的用自研网关。这种“接口统一、实现可替换”的场景,天生就适合做Starter封装。
我的设计目标是这样的:
- 调用方只需在
application.yml里配置demo.sms.provider、demo.sms.access-key、demo.sms.secret-key。 - 业务代码里注入一个
SmsSender接口,直接调用send(phone, content)方法。 - 底层对接哪一个服务商,由配置决定,业务代码完全感知不到。
- 如果调用方已经自己定义了一个
SmsSenderBean,我们的Starter自动失效,绝不覆盖用户的Bean。
这个目标里其实就包含了两个核心原理的实践点:配置绑定和条件装配。下面我一步步来实现。
2.2 工程结构与依赖选型
整个Starter作为一个独立模块,不能放在业务项目里,因为它的目标是复用。我建议的目录结构如下:
demo-sms-spring-boot-starter/ ├── pom.xml ├── src/main/java/ │ └── com/example/demo/sms/ │ ├── SmsSender.java // 统一接口 │ ├── SmsProperties.java // 配置属性类 │ ├── AliSmsSender.java // 阿里云实现 │ ├── TencentSmsSender.java // 腾讯云实现 │ ├── SmsAutoConfiguration.java // 自动配置类 │ └── DemoSmsAutoConfiguration.java // 额外扩展 └── src/main/resources/ └── META-INF/spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports依赖方面,SpringBoot官方提供的spring-boot-autoconfigure和spring-boot-starter是必须的,但要注意一个问题:Starter的pom.xml不能依赖具体的短信服务商SDK,而是应该让使用方自行引入。这样做的原因是,如果把所有服务商的SDK都依赖进来,会让整个项目变得臃肿,也会引入大量调用方根本用不到的传递依赖。
提示:写Starter时,对被封装对象的依赖要格外克制。我这个Starter只依赖SpringBoot基础能力,短信服务的具体SDK由使用方按需添加,这和SpringBoot官方
spring-boot-starter-data-redis不直接引入连接池驱动是同一个道理。
pom.xml核心依赖如下:
<properties> <spring-boot.version>3.2.5</spring-boot.version> </properties> <dependencies> <!-- 自动配置支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- 配置元数据生成器,写配置时有提示 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${spring-boot.version}</version> <optional>true</optional> </dependency> <!-- 日志统一抽象,避免直接绑死logback或log4j --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> </dependencies>spring-boot-configuration-processor这个依赖容易被忽略,但特别有用。把它加上后,IDE在编写application.yml时就会自动提示你在Starter里定义的那些配置项,这能大幅提升使用方的开发体验,属于投入极小收益极大的做法。
3. 核心代码实现与原理逐层剖析
3.1 属性绑定类:配置项与Java对象的桥梁
先写属性绑定类SmsProperties。这个类的职责很简单:读取application.yml中demo.sms前缀下的所有配置项。
@ConfigurationProperties(prefix = "demo.sms") public class SmsProperties { /** 服务商类型:ali / tencent / mock */ private String provider = "mock"; /** 接入凭证 */ private String accessKey; /** 接入密钥 */ private String secretKey; /** 发送超时时间/秒 */ private int timeout = 5; /** 是否启用短信发送功能 */ private boolean enabled = true; // 省略 getter/setter }这里有几个细节值得展开说说。
第一个是prefix的命名。我用了demo.sms而不是sms,因为这种公共组件最好带一个业务前缀,避免跟其他组件撞车。业界常见做法是公司域名+模块名,比如com.example.xxx,这样在大型项目里不会出现两个组件抢同一个配置前缀的尴尬。
第二个是默认值的作用。provider = "mock"表示如果没有配置服务商,默认走一个什么都不做的模拟实现,这个设计很实用——在本地开发、单元测试场景里,不需要真正对接短信服务商,也能把整个链路跑起来。
第三个是配置绑定的原理。@ConfigurationProperties注解本身只是在Spring容器里标记了一个“可被绑定的目标”,真正干活的是ConfigurationPropertiesBindingPostProcessor。它在Bean创建后、初始化前介入,把Environment里的配置值转换成对应类型并填充到属性上。
平时在业务项目里到处用的@Value("${xxx}"),本质上走的是另一条路径——AutowiredAnnotationBeanPostProcessor处理的@Value解析。两条链路不同,但结果类似,都是把Environment里的配置值注入到Bean属性上。@ConfigurationProperties更适合定义一组关联配置,代码可读性远高于一堆散落的@Value。
3.2 统一接口与多实现:为条件装配做准备
有了配置类,接下来定义统一发送接口和两个服务商实现。
public interface SmsSender { /** * 发送短信 * * @param phone 接收手机号 * @param content 短信内容 * @return 发送结果 */ SendResult send(String phone, String content); }接口本身不复杂,关键在于给接口配上不同实现,然后在自动配置类里根据条件决定装配哪个实现。这就是SpringBoot里非常经典的“策略模式+条件装配”组合拳。阿里云实现和腾讯云实现的差异可以体现在内部调用的SDK和API上,对外暴露的send方法签名完全一致。
public class AliSmsSender implements SmsSender { private static final Logger log = LoggerFactory.getLogger(AliSmsSender.class); private final SmsProperties properties; public AliSmsSender(SmsProperties properties) { this.properties = properties; } @Override public SendResult send(String phone, String content) { // 实际场景中,这里是调用阿里云短信SDK的代码 // 为了演示方便,用日志代替真实HTTP调用 log.info("[阿里云短信] 发送中: 手机号={}, 内容={}", phone, content); return SendResult.success(properties.getProvider()); } }腾讯云实现差不多,只不过日志前缀变成了“腾讯云短信”。这里我不贴完整代码,因为核心不在短信API怎么调,而在于**如何让Spring容器按照配置选择装配哪一个实现。**这是Spring的依赖注入容器最擅长的事,我们只需要在@Bean方法上做好条件判断。
3.3 自动配置类:条件注解与Bean装配的主战场
自动配置类是Starter的灵魂。先看代码:
@AutoConfiguration @ConditionalOnClass(SmsSender.class) @ConditionalOnProperty(prefix = "demo.sms", name = "enabled", havingValue = "true", matchIfMissing = true) @EnableConfigurationProperties(SmsProperties.class) public class SmsAutoConfiguration { @Bean @ConditionalOnMissingBean(SmsSender.class) @ConditionalOnProperty(prefix = "demo.sms", name = "provider", havingValue = "ali", matchIfMissing = false) public SmsSender aliSmsSender(SmsProperties properties) { return new AliSmsSender(properties); } @Bean @ConditionalOnMissingBean(SmsSender.class) @ConditionalOnProperty(prefix = "demo.sms", name = "provider", havingValue = "tencent", matchIfMissing = false) public SmsSender tencentSmsSender(SmsProperties properties) { return new TencentSmsSender(properties); } @Bean @ConditionalOnMissingBean(SmsSender.class) @ConditionalOnProperty(prefix = "demo.sms", name = "provider", havingValue = "mock", matchIfMissing = true) public SmsSender mockSmsSender(SmsProperties properties) { return new MockSmsSender(properties); } }这大概是整个Starter里信息密度最高的一段代码,我来逐个注解拆解。
@AutoConfiguration是SpringBoot 2.7之后用来替代@Configuration+ 自动配置语义的新注解。它的作用有两个:一是向SpringBoot明确“我是一个自动配置类”,二是支持设置after/before排序属性。这个注解是AutoConfigurationImportSelector在加载时识别自动配置类的依据。
@ConditionalOnClass(SmsSender.class)这个条件判断当前应用环境中是否包含某个类。我在这里用SmsSender.class只是做一个示例,实际场景中更常用的写法是判断第三方SDK的类,比如@ConditionalOnClass(com.aliyuncs.DefaultAcsClient.class),意思是“只有当你引入了阿里云SDK依赖时,这个配置才生效”。SpringBoot广泛采用这种“classpath存在性检测”而不是显式的Profile开关,因为引入依赖本身就表达了一个意图。
@ConditionalOnMissingBean(SmsSender.class)是这个配置里最关键的注解。它表达的含义是:如果容器里已经存在了SmsSender类型的Bean,我就不创建了。这就是用户自定义优先原则的底层实现。如果使用方在业务代码里自己实现了一个SmsSender,SpringBoot会优先使用用户的实现,用户的Bean不会被我们的默认实现覆盖。写公共组件时,这个注解一定不能漏。
@ConditionalOnProperty通过配置项的存在与否、具体值来决定配置是否生效。三个@Bean方法各自绑定一个provider配置值,havingValue精确匹配时才装配对应的短信实现。如果配置没有写provider,最后那个matchIfMissing = true会让mockSmsSender生效,保证Starter在最简配置下也不会报“找不到Bean”的错误。
@EnableConfigurationProperties(SmsProperties.class)的作用是把SmsProperties注册为Spring容器中的一个Bean。有人问过“我用了@ConfigurationProperties为什么还需要@EnableConfigurationProperties”,答案很简单:@ConfigurationProperties只是声明了“这个类可以绑定配置”,但Spring默认不会去扫描它。@EnableConfigurationProperties显式告诉Spring“把这个类注册为Bean并执行绑定”,两者缺一不可。在配置类上写@EnableConfigurationProperties(SmsProperties.class)是官方推荐的写法。
3.4 注册自动配置类:SPI声明文件的两种写法
配置类和代码都写完了,还剩最后一步:告诉SpringBoot“我这里有自动配置类”。这一步是很多新手最容易漏的。
在SpringBoot 3.x版本中,需要在src/main/resources/META-INF/spring/目录下创建文件org.springframework.boot.autoconfigure.AutoConfiguration.imports,内容非常简单:
com.example.demo.sms.SmsAutoConfiguration如果用的是SpringBoot 2.7之前的版本,则要写在META-INF/spring.factories里:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.demo.sms.SmsAutoConfiguration两种写法的本质是一样的,都是SPI机制。SpringBoot的SpringFactoriesLoader和AutoConfigurationImportSelector会通过类加载器扫描所有jar包里的声明文件,将类名加载出来,再去执行条件判断。
注意:SpringBoot 2.7官方已经将
spring.factories标记为废弃,SpringBoot 3.x 不再支持通过spring.factories注册自动配置类。如果你用的是3.x版本却还在按老教程写spring.factories,启动时会发现配置类根本没被加载。
这一步经常出问题,排查方法也很简单:在启动类上加--debug参数,SpringBoot启动后会打印一份自动配置报告,里面详细列出了“哪些自动配置类生效”“哪些没有生效”“没有生效的原因是什么”。我在后文会专门讲这个排查技巧。
3.5 条件注解原理:从源码层面解读@Conditional
把原理部分再往深处挖一层。很多人在项目里用过@ConditionalOnXxx,但不清楚它是怎么被SpringBoot解析执行的。这背后其实是一个被很多人忽略的概念:Condition接口。
Spring框架在4.0版本引入了@Conditional注解和Condition接口。@Conditional可以标注在类上或@Bean方法上,它接收一个Condition实现类数组。Spring在处理配置类时,会先评估标注的Condition,全部返回true才会继续处理后续逻辑。SpringBoot在spring-boot-autoconfigure包里对@Conditional做了大量扩展,把常用的判断逻辑封装成了几个开箱即用的注解:
@ConditionalOnClass/@ConditionalOnMissingClass:classpath里有没有某个类。@ConditionalOnBean/@ConditionalOnMissingBean:容器里有没有某个Bean。@ConditionalOnProperty:配置项是否存在、是否等于某个值。@ConditionalOnExpression:SpEL表达式求值。@ConditionalOnWebApplication/@ConditionalOnNotWebApplication:是不是Web应用环境。
每个注解背后都对应一个Condition实现,比如OnClassCondition、OnBeanCondition、OnPropertyCondition。SpringBoot在解析自动配置类时,会对类上所有的@Conditional注解逐一求值,这个过程发生在ConfigurationClassParser处理配置类的阶段,早于@Bean方法的实际执行。
真正的高阶玩法是自定义Condition。比如我想写一个只在“Linux系统”上才生效的配置类,可以自己实现Condition接口:
public class LinuxCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String osName = context.getEnvironment().getProperty("os.name", ""); return osName.toLowerCase().contains("linux"); } }然后在配置类上用@Conditional(LinuxCondition.class)即可。这个能力和SpringBoot提供的那些注解本质上是同一套机制,理解了这一点,以后遇到任何奇怪的“为什么这个Bean没生效”问题,都能顺着这个思路去查。
4. 完整的可复用Starter骨架与进阶增强
4.1 从短信Starter到通用组件:提炼公共范式
短信Starter写完了,但它的价值不止于短信本身。绝大多数内部Starter都逃不出同一个套路:属性绑定、条件装配、SPI声明。我把这个套路提炼成一个可复用的四步法,任何团队成员照着这个步骤都能快速开发自己的Starter:
- 定义属性类:确定配置前缀,设计配置项清单,为每个配置项设置合理的默认值。
- 定义服务接口:把对外暴露的能力抽象成接口,内部实现可以随时替换。
- 编写自动配置类:使用
@AutoConfiguration+ 条件注解声明装配逻辑。 - 注册SPI声明:在
META-INF/spring/下创建AutoConfiguration.imports文件,列出自动配置类全限定类名。
我之前在团队里用这套方法分别封装过对象存储组件、分布式ID生成器、通用脱敏工具,每次都只需几小时就能完成。核心原因是思路清晰,每类组件需要处理的差异点很少:对象存储要处理的就是@ConditionalOnProperty的bucket、endpoint、region等配置差异;分布式ID生成器要处理的是@ConditionalOnClass判断使用方的zookeeper或redis驱动是否存在。
4.2 进阶增强一:用@AutoConfigureAfter控制加载顺序
说回短信Starter。写配置类的时候可能遇到一个需求:如果使用方项目里引入了某个“全局短信拦截器”组件,短信发送必须晚于拦截器初始化,否则拦截器装配时就会因为没有SmsSender而报错。
这种场景需要控制自动配置类的加载顺序。SpringBoot提供了三个注解:
@AutoConfigureOrder:定义自动配置类的整体顺序,数值越小越先加载。@AutoConfigureBefore:声明本配置类必须在目标配置类之前加载。@AutoConfigureAfter:声明本配置类必须在目标配置类之后加载。
用法很直白:
@AutoConfiguration @AutoConfigureAfter(SomeOtherAutoConfiguration.class) @ConditionalOnClass(SmsSender.class) public class SmsAutoConfiguration { // ... }这里要注意,这三个注解控制的是自动配置类之间的先后顺序,不是Bean的依赖关系。Spring的依赖注入本身就能处理@Bean方法之间通过参数表达的直接依赖,排序注解存在的意义是处理那些没有直接依赖、但执行有先后要求的场景。比如DataSourceAutoConfiguration就是通过@AutoConfigureBefore(MyBatisAutoConfiguration.class)确保数据源先于MyBatis创建。
4.3 进阶增强二:健康检查与自定义注解
生产环境使用Starter组件,还有一个容易被忽略但很重要的东西:健康检查。如果短信服务商暂时不可用、凭证失效,调用方需要能第一时间发现,而不是等到用户反馈收不到验证码才去查。给Starter加上健康检查其实不复杂,核心是让SmsSender实现一个HealthIndicator:
@Component public class SmsHealthIndicator implements HealthIndicator { private final SmsSender smsSender; public SmsHealthIndicator(SmsSender smsSender) { this.smsSender = smsSender; } @Override public Health health() { // 实际使用时可以在这里做一次连通性测试,比如发送一条空内容短信给测试号码 boolean isHealthy = smsSender.ping(); return isHealthy ? Health.up().withDetail("sms", "available").build() : Health.down().withDetail("sms", "service unavailable").build(); } }SpringBoot的Actuator会自动扫描容器里所有HealthIndicator接口实现,把它们聚合到/actuator/health端点的输出结果里。这样监控系统只需要定期请求健康检查接口,就能同时感知短信组件的状态。
4.4 使用方视角:引入Starter后怎么配置
开发完Starter不能只自己测试,还要站在使用方的角度过一遍完整流程。使用方引入Starter后的操作分三步:
第一步,在pom.xml里添加依赖。
<dependency> <groupId>com.example.demo</groupId> <artifactId>demo-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>第三步,在application.yml里做配置。
demo: sms: provider: ali access-key: LTAI5txxxxx secret-key: xxxxxxxxx timeout: 10 enabled: true第四步,在业务代码里注入并使用。
@Service public class LoginService { private final SmsSender smsSender; public LoginService(SmsSender smsSender) { this.smsSender = smsSender; } public void sendVerifyCode(String phone) { String code = generateCode(); smsSender.send(phone, "您的验证码是" + code); } }到这里,一个完整的Starter从开发到落地使用的整个链路就走通了。
5. 常见问题与排查技巧实录
5.1 自动配置没生效:八成是SPI文件写错了
这是手写Starter遇到最多的问题。代码全部写完,SpringBoot启动后日志里却找不到自己写的配置类,SmsSender注入直接报错。排查思路按顺序走:
第一步,确认SPI文件的位置和文件名是否正确。SpringBoot 3.x要求文件在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports下,这是一个很长的文件名,连字母大小写都不能错。加粗提示:这个文件名里的路径是org.springframework.boot.autoconfigure,不是org.springframework.boot。
第二步,用--debug参数启动应用,看自动配置报告里有没有相关信息。SpringBoot启动后会在控制台打印一份详细的自动配置报告,格式类似:
================================= CONDITIONS EVALUATION REPORT ================================= Positive matches: com.example.demo.sms.SmsAutoConfiguration - matched Negative matches: (此处列出没匹配上的配置类及原因)看到SmsAutoConfiguration出现在Positive matches里就说明加载成功了。如果它出现在Negative matches里,报告里会写明没匹配上的原因,比如@ConditionalOnClass没找到指定类、@ConditionalOnProperty配置不满足等。
第三步,检查是不是构建产物没更新。多模块项目中,如果Starter模块改了代码没有先mvn install到本地仓库,使用方项目引入的仍然是旧版本jar包,自然看不到效果。
5.2 配置项老是绑定不上:默认值和空值问题
另一个常见问题是,使用方明明在application.yml里写了配置,但代码里的属性仍然是null或默认值。
最常见的场景是配置前缀写错。@ConfigurationProperties(prefix = "demo.sms")对应application.yml里的写法是:
demo: sms: provider: ali注意前缀必须完全匹配,少了demo或者多写一层的demo.sms.provider都会导致绑定失败。SpringBoot的Relaxed binding允许一定程度的“松散绑定”,比如demo.sms.access-key也可以写成demo.sms.accessKey,但前缀那一层不能多也不能少。
其次要检查属性类有没有对应的getter/setter。SpringBoot的配置绑定走的是JavaBean规范,需要setter方法才能赋值。如果你用的是Lombok,别忘了在属性类上加@Data或者@Setter。这个问题很隐蔽,因为编译不会报错,启动也不会报错,只是一直是null。
还有一点容易被忽略:如果使用方把配置写在application.properties里,注意不要用单引号包裹配置值。demo.sms.provider='ali'会把'ali'整个字符串赋值进去,havingValue = "ali"自然匹配不上。
5.3 开关Switch与兜底策略:让Starter更健壮的几个细节
把几个实践里总结出的细节一并说说。
第一,@ConditionalOnMissingBean不能漏。漏掉它的后果是:一旦使用方自己定义了SmsSender实现,Starter里的默认实现会一起注册,容器里出现两个同类型Bean,注入时直接报NoUniqueBeanDefinitionException。这种情况极为常见,我之前排查过好几个同事的报错,最后发现都是自动配置类少写了这个条件。
第二,@Bean方法上的@ConditionalOnMissingBean和类上的同名注解作用域不同。类上只会在配置类处理阶段判断一次,@Bean方法上的注解则针对每个Bean方法单独判断。实践中最稳的用法是在@Bean方法上也标注,双保险。
第三,日志输出一定要用LoggerFactory.getLogger(SmsSender.class)这种基于类的logger,不要用System.out.println。Starter是公共组件,会运行在各种日志框架组合的环境里,使用SLF4J接口而不是直接绑定某个具体实现,是最基本的要求。
第四,给SmsProperties里的字段加Javadoc注释。这不是形式主义,SpringBoot会解析这些注释生成spring-configuration-metadata.json文件,在IDE里编写配置时会有完整的提示信息,包括每个配置项的含义、类型、默认值。公共组件面向的是团队里其他人,好的使用体验能省下大量答疑成本。
6. 手写Starter对理解SpringBoot源码的帮助
写完之后回头看,手写Starter不只是解决“封装公共组件”这一个业务问题,它对理解SpringBoot源码的助益是泛而深远的。我在没有亲手写Starter之前,读自动配置相关的源码总是有一种“看了又好像没看”的飘忽感。为什么?因为脑子里缺少一个具体的场景把这些零散的类串起来。写了一遍短信Starter之后,源码里那些概念突然全都有了实体感。
举个例子,之前读AutoConfigurationImportSelector的源码,里面有个getCandidateConfigurations方法,从jar包扫描配置声明文件。如果没有手写Starter的经验,我只会把它当成“加载配置列表”的简单逻辑。但自己写了一个Starter之后,我知道了SpringBoot在启动时到底经历了什么:先通过SpringFactoriesLoader.loadFactoryNames加载候选类名列表,再经过AutoConfigurationSorter排序,最后交给ConfigurationClassPostProcessor处理。这些类各自负责什么,在出问题时大概往哪个方向查,心中一目了然。
另一个受益点是条件注解不再神秘。日常业务开发中也会用到@ConditionalOnProperty、@ConditionalOnBean,但大多数时候只是照葫芦画瓢。手写Starter要求你在多个维度上组合使用这些注解,每个注解的生效时机、判断粒度和边界情况都要想清楚,这比读十遍文档都管用。比如我为了搞明白@ConditionalOnBean判断的“当前Bean定义注册时点”,专门翻了OnBeanCondition的源码,才理解为什么@ConditionalOnBean在@Bean方法上使用时经常不按预期生效——这个问题在业务代码里几乎碰不到,但在写Starter时避不开。
所以我的建议是:不管团队里有没有内部组件化的需求,都值得亲手写一个Starter练手。不需要选多复杂的业务,哪怕只是一个“当前时间格式化工具”的Starter,把自动配置类、SPI声明、属性绑定、条件判断这几个环节完整走一遍,收益都远大于看一遍源码分析文章。纸上得来终觉浅,这句老话放在源码学习上尤其扎心。
最后再分享一个我自己踩过的坑:第一次写Starter的时候,我把SmsAutoConfiguration上的@ConditionalOnProperty和@Bean方法上的@ConditionalOnProperty混用了,导致本地测试正常、部署到测试环境后所有短信都变成了mock发送。最后排查半天才发现是测试环境的配置中心里多写了一层demo前缀,配置层级对不上。那次之后我养成了一个习惯:Starter里涉及配置项的条件判断,一律在@Bean方法上显式声明,类级别只保留与配置无关的整体开关,这样出问题时定位范围能缩小一大半。这个习惯一直延续到现在,也希望它能帮你少走几步弯路。