场景:从"会用 starter"到"会造 starter"

团队里有一段每个微服务都要复制的样板代码:统一的日志埋点、统一的限流客户端、统一的加解密工具。每接入一个新服务,就要拷一遍配置、声明一堆 Bean。这正是 Spring Boot Starter 要解决的问题——把"一组能力 + 默认配置 + 自动装配"打包成一个依赖,别人引入即用。会写 starter,是从"框架使用者"迈向"基础设施开发者"的关键一步。

机制:starter 到底由什么构成

一个标准的对外 starter 通常拆成两个模块(社区惯例):

  • xxx-spring-boot-autoconfigure:放真正的自动配置类、@ConfigurationProperties、条件注解。这是大脑。
  • xxx-spring-boot-starter:几乎是空的,只在 pom.xml 里依赖 autoconfigure 模块和必要的第三方库。它只是个"依赖聚合点",方便使用者一行引入。

命名约定:官方 starter 叫 spring-boot-starter-xxx,第三方应反过来叫 xxx-spring-boot-starter,避免与官方命名空间冲突。

自动装配能生效,靠的是前文讲过的那套机制:候选清单文件 + 条件评估 + 属性绑定。我们要做的就是把自己的配置类登记进候选清单。

实战:写一个限流能力 starter

假设我们要做一个简单的"接口限流"starter,使用者引入后,通过 application.yml 就能开关和调参。

第一步:定义配置属性

1
2
3
4
5
6
7
8
9
@ConfigurationProperties(prefix = "demo.ratelimit")
public class RateLimitProperties {
/** 是否启用限流 */
private boolean enabled = true;
/** 每秒允许的请求数 */
private int permitsPerSecond = 100;

// getter / setter 省略
}

prefix 决定了使用者在 yml 里怎么写:demo.ratelimit.permits-per-second: 200。Spring Boot 的宽松绑定(relaxed binding)会自动把 permits-per-second(kebab-case)映射到 permitsPerSecond(camelCase)。

第二步:写自动配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
@AutoConfiguration
@ConditionalOnClass(RateLimiter.class) // 类路径有限流核心类才装配
@EnableConfigurationProperties(RateLimitProperties.class)
@ConditionalOnProperty(prefix = "demo.ratelimit",
name = "enabled", havingValue = "true", matchIfMissing = true)
public class RateLimitAutoConfiguration {

@Bean
@ConditionalOnMissingBean // 用户没自定义才用我的默认实现
public RateLimiter rateLimiter(RateLimitProperties props) {
return new RateLimiter(props.getPermitsPerSecond());
}
}

四个注解各司其职:

  • @AutoConfiguration:标记这是一个自动配置类(较新版本专用,内部包含 @Configuration(proxyBeanMethods = false),关闭了 @Bean 方法的代理以提升性能)。
  • @ConditionalOnClass:只有当使用者的 classpath 上真的有限流核心类时才装配——避免把无关依赖强加给别人。
  • @EnableConfigurationProperties:让 RateLimitProperties 生效并注册为 Bean。
  • @ConditionalOnProperty(... matchIfMissing = true):提供"默认开启、可显式关闭"的开关;matchIfMissing=true 表示用户不写这个配置项时也算匹配。
  • @ConditionalOnMissingBean:留出覆盖钩子——使用者若自己定义了 RateLimiter,我们就退让。这是优秀 starter 的标志:有默认值,但不剥夺定制权。

第三步:登记候选清单(最关键的一步)

新建文件 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

1
com.demo.ratelimit.RateLimitAutoConfiguration

每行一个全限定类名。这一步是 starter 能"自动"生效的唯一开关,漏了它,无论注解写得多漂亮,配置类都不会被加载——这是新手写 starter 最常见的"明明都对了却不生效"的原因。

早期(Spring Boot 2.x)对应的是 META-INF/spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration=...,新项目建议用 .imports 文件。

第四步:starter 聚合模块

xxx-spring-boot-starterpom.xml 里只需:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.demo</groupId>
<artifactId>ratelimit-spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 限流核心库等运行时必需依赖 -->
</dependencies>

使用者只依赖这个 starter,传递依赖会把 autoconfigure 也带进来。

第四步补充:配置元数据提示(可选但加分)

引入 spring-boot-configuration-processor(optional/provided 作用域),编译期会生成 META-INF/spring-configuration-metadata.json,这样使用者在 IDE 里写 demo.ratelimit. 时能弹出自动补全和文档提示。一个有提示的 starter,体验上和官方 starter 没有区别。

工程权衡

依赖作用域要克制。 autoconfigure 模块对第三方库的依赖,凡是"用户可能不需要"的,都应设为 optional 或让 @ConditionalOnClass 来兜底。否则你的 starter 会把一堆传递依赖塞进别人的项目,污染依赖树、放大冲突面。optional=true 配合 @ConditionalOnClass 是经典组合:依赖不强制传递,类存在才装配。

proxyBeanMethods 的取舍。 @AutoConfiguration 默认 proxyBeanMethods=false,意味着配置类不被 CGLIB 代理,@Bean 方法之间互相调用不再返回同一单例(而是各自 new)。好处是启动更快、内存更省;代价是如果你的配置类里 @Bean a() 内部调用了 b() 期望拿同一个 Bean,会失效。正确写法是把 b 作为方法参数让容器注入,而不是方法内直调。

装配顺序。 若你的 starter 依赖另一个 starter 先装配好的 Bean(比如要拿到别人的 DataSource),用 @AutoConfigureAfter 声明顺序,否则可能因 @ConditionalOnBean 在评估时对方 Bean 还没注册而判负。

常见误区与踩坑

  • 漏写 .imports 文件:最高频。配置类没进候选清单,等于没写。
  • @Component 而非 imports 登记:有人图省事给自动配置类加 @Component,寄希望于使用者的包扫描扫到它。但 starter 的包名通常不在使用者的 @ComponentScan 范围内,扫不到。必须走 imports 机制。
  • @ConditionalOnMissingBean 与多 Bean 冲突:如果使用者环境里已存在同类型 Bean(可能来自另一个 starter),你的默认 Bean 会静默退让,排查时要看 --debug 的条件报告。
  • 强依赖污染:把本该 optional 的库设成 compile,导致使用者被迫引入用不到的传递依赖。
  • 配置项命名不规范:prefix 用了大写或下划线,破坏宽松绑定预期,使用者在 yml 里怎么写都绑不上。

小结

手写 starter 的核心公式是:@ConfigurationProperties(可配置)+ 自动配置类(默认装配)+ 条件注解(按需生效)+ .imports 候选清单(注册入口),再用 @ConditionalOnMissingBean 留下覆盖钩子、用 optional 依赖保持克制。四步走完,你就把一段重复代码升级成了团队级的基础设施组件。记住那句话:好的 starter 提供合理默认,但永远把最终决定权留给使用者。