场景:从"会用 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 |
|
prefix 决定了使用者在 yml 里怎么写:demo.ratelimit.permits-per-second: 200。Spring Boot 的宽松绑定(relaxed binding)会自动把 permits-per-second(kebab-case)映射到 permitsPerSecond(camelCase)。
第二步:写自动配置类
1 |
|
四个注解各司其职:
@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.factories里org.springframework.boot.autoconfigure.EnableAutoConfiguration=...,新项目建议用.imports文件。
第四步:starter 聚合模块
xxx-spring-boot-starter 的 pom.xml 里只需:
1 | <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 提供合理默认,但永远把最终决定权留给使用者。