自定义Spring Boot Starter全流程
目录
- [1. Starter 的构成](#1. Starter 的构成)
- [2. 常用 Starter 盘点](#2. 常用 Starter 盘点)
- [3. 命名规范](#3. 命名规范)
- [4. 双模块结构](#4. 双模块结构)
- [5. 手写一个 Starter](#5. 手写一个 Starter)
- [6. 常见踩坑](#6. 常见踩坑)
- [7. 小结](#7. 小结)
我们在开发过程中,当需要用到 @RestController、@RequestMapping 这些 Web 开发常用的功能时,只需要往 pom.xml 里加一个 spring-boot-starter-web,相关的依赖就会一起引入进来。Tomcat、Jackson 这些组件也不用再单独配置。
看起来只是引入了一个依赖,但 Starter 本身其实没有什么特殊的地方,本质上还是一个 Maven 依赖。它主要负责把一组相关依赖整理到一起,而真正负责自动配置、创建 Bean 的,是 Spring Boot 后面的自动配置机制。
Spring Boot 本身提供了很多常用的 Starter,理解了它们背后的结构之后,我们就可以在需要使用的时候自定义 Starter。下面先看看常用 Starter 的组成,再用一个短信服务的例子,从零实现一个自己的 Starter。
1. Starter 的构成
拆开一个 starter,里面有两部分:依赖声明和自动配置。
第一部分是 pom 里的依赖声明。spring-boot-starter-web 的 pom 里没有写一行业务代码,它只是声明了一堆传递依赖。简化来看,它做的事大致等价于:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
引入这一个 starter,就等于把这一整套互相配套的依赖打包引了进来。starter 在这里解决的是"别人用你的组件时,到底要手动引哪些 jar"这个老问题。
第二部分是自动配置类。这部分逻辑不在 starter 本体里,而在它依赖的 autoconfigure 模块中,通过 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件登记。Spring Boot 启动时读这个文件、加载配置类、按条件装配,这套机制在之前的《SpringBoot 自动配置原理》里已经拆过,这里不再重复。
starter 主要负责聚合依赖,autoconfigure 模块负责自动配置。两者组合起来,就是一个完整的自定义 starter。
2. 常用 Starter 盘点
Spring Boot 官方提供了大量 starter,日常开发中比较常见的有下面这些:
| Starter | 引入后带来什么 |
|---|---|
| spring-boot-starter-web | 内嵌 Tomcat、Spring MVC、Jackson,写 REST 接口 |
| spring-boot-starter-data-redis | RedisTemplate、连接池,读写 Redis |
| spring-boot-starter-data-jpa | Hibernate、Spring Data JPA,操作数据库 |
| spring-boot-starter-security | 安全框架,登录鉴权、权限控制 |
| spring-boot-starter-aop | AOP 切面支持,日志、事务增强 |
| spring-boot-starter-validation | 参数校验,@NotNull、@Size 等注解生效 |
| spring-boot-starter-actuator | 健康检查、监控指标端点 |
| spring-boot-starter-test | JUnit、Mockito 等测试框架集 |
可以看到,官方 starter 的命名都直接对应它解决的问题。
3. 命名规范
官方对 starter 的命名有硬性约定,分两种:
- 官方 starter 用
spring-boot-starter-*前缀 - 第三方和自定义 starter 用
*-spring-boot-starter后缀
自定义 starter 不建议使用 spring-boot-starter-*。这个前缀留给 Spring 官方,第三方组件通常用 *-spring-boot-starter,可以避免命名混淆,也避免将来 Spring 推出同名官方 starter 时产生冲突。
MyBatis 官方的 mybatis-spring-boot-starter、阿里云出的 dubbo-spring-boot-starter,都是后缀约定。所以你的短信 starter 应该叫 sms-spring-boot-starter,而不是 spring-boot-starter-sms。
4. 双模块结构
一个规范的自定义 starter,通常拆成两个 jar,而不是一个。
sms-spring-boot-starter(壳,只声明依赖)
│ 依赖
▼
sms-spring-boot-starter-autoconfigure(核心)
│
├── SmsProperties 配置项
├── SmsAutoConfiguration 自动配置类
└── AutoConfiguration.imports 注册清单
拆成两个模块,主要是为了职责隔离。starter 本身不放业务代码,只负责聚合依赖;autoconfigure 模块负责属性绑定、Bean 创建和条件装配。两个模块分开后,使用方也可以根据需要只引入 autoconfigure,而不是强制接受 starter 里的全部依赖。比如一个项目已经手动管理了短信 SDK 的依赖,只想复用你的自动配置逻辑,它可以只引 autoconfigure 模块、跳过 starter。
5. 手写一个 Starter
短信服务是内部服务里很典型的一个:多个业务线都要用,接入方式固定。把它封装成一个 starter,各项目引进来、填好配置就能用。我们用这个场景来举例实现:
5.1 定义可配置项
先把可配置的属性抽出来,用 @ConfigurationProperties 绑定到 yml:
java
package com.example.sms;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
// 是否启用短信服务,默认开启
private boolean enabled = true;
// 短信平台的 appKey / appSecret
private String appKey;
private String appSecret;
// 短信签名
private String signName;
// 短信平台接口地址
private String endpoint = "https://sms.example.com";
// 省略 getter / setter
}
用户之后在 yml 里写 sms.app-key、sms.sign-name,Spring 会自动绑定到这个类上。prefix = "sms" 就是 yml 里那一层键。yml 里的 app-key 会自动绑定到 Java 的 appKey,中划线转成驼峰,这叫松散绑定,不用你手动处理。
5.2 写服务类和自动配置类
服务类封装真实的发送逻辑:
java
package com.example.sms;
public class SmsService {
private final SmsProperties properties;
public SmsService(SmsProperties properties) {
this.properties = properties;
}
public void send(String phone, String content) {
// 真实场景里这里调用短信平台的 HTTP API
System.out.printf("发送短信到 %s,签名:%s,内容:%s%n",
phone, properties.getSignName(), content);
}
}
自动配置类负责在启动时创建这个 Bean:
java
package com.example.sms;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "sms", name = "enabled",
havingValue = "true", matchIfMissing = true)
public SmsService smsService(SmsProperties properties) {
return new SmsService(properties);
}
}
这里三个注解分别负责不同的事:@EnableConfigurationProperties 注册并绑定配置属性,@ConditionalOnMissingBean 控制用户自定义 Bean 后的覆盖关系,@ConditionalOnProperty 控制短信服务是否启用。matchIfMissing = true 表示用户不写 sms.enabled 这个配置时,默认也启用。
如果你的 starter 包装的是某个第三方 SDK,通常还要加一个 @ConditionalOnClass(SdkClient.class),避免没引入 SDK 时因为缺类直接报错。本例服务类是自包含的,所以省掉了。
5.3 注册到 imports 文件
配置类写好了,还要告诉 Spring Boot 去哪里找它。在 autoconfigure 模块的 resources 下建这个文件:
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容就是配置类的全限定名,一行一个:
com.example.sms.SmsAutoConfiguration
这是 Spring Boot 3.x 的写法。Spring Boot 2.x 使用的是 META-INF/spring.factories,两者格式不同。
5.4 组装 starter 模块
现在写那个"壳"。新建一个 starter 模块,pom 里只做一件事:依赖 autoconfigure 模块。
xml
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-starter-autoconfigure</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
这个模块没有任何 Java 代码。它存在的意义,就是让使用方只引一个 sms-spring-boot-starter,传递依赖会自动把 autoconfigure 模块拉进来。
5.5 引入并测试
在另一个项目里引入这个 starter:
xml
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
填配置:
yaml
sms:
app-key: LTAI5txxxx
app-secret: xxxxxxxxxx
sign-name: 我的应用
然后直接注入使用:
java
@Service
public class UserService {
@Autowired
private SmsService smsService;
public void sendCode(String phone) {
smsService.send(phone, "您的验证码是 123456");
}
}
没有手动 new,没有手动配 Bean,SmsService 已经在容器里了。封装到这里,其他项目引进来就能直接复用。
6. 常见踩坑
第一个坑:配置绑定不上。写了 sms.app-key,但注入的 SmsProperties 里 appKey 是 null。多半是忘了在配置类上加 @EnableConfigurationProperties(SmsProperties.class)。只标 @ConfigurationProperties 是不够的,它不会自己生效,需要有人把它注册进去。
第二个坑:imports 文件路径写错。这个文件的目录层级是 META-INF/spring/,中间那个 spring 目录漏掉,或者拼成了 META-INF/services,Spring 就找不到你的配置类。这个问题比较隐蔽:项目通常不会直接报错,只是对应的 Bean 没有创建。
第三个坑:条件注解写太死。比如 @ConditionalOnProperty 写了 havingValue = "true" 却忘了 matchIfMissing = true,用户只要没在 yml 里显式写 sms.enabled=true,这个 Bean 就不会被创建,排查半天才发现是条件没满足。
第四个坑:把配置类直接写在 starter 模块里。虽然也能跑,但破坏了双模块的分工,starter 和 autoconfigure 纠缠在一起,后面想单独复用配置逻辑或单独引依赖就做不到了。
7. 小结
starter 的核心是:聚合依赖,加自动配置。starter 模块负责把依赖一起引进来,autoconfigure 模块负责把 Bean 配好,两个模块打包成一个依赖,团队里的其他项目引入后就能直接使用。
不过,也不是所有内部工具都值得做成 starter。如果一个工具只有一个项目在用,直接写个 @Configuration 就够,做成 starter 反而多了一层 jar 维护成本。只有当它被多个服务复用、且配置有明确的默认值时,才值得走"双模块 + imports 文件"这套流程。判断标准就是:有没有第二个项目需要用它。