自定义Spring Boot Starter全流程

自定义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-keysms.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,但注入的 SmsPropertiesappKey 是 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 文件"这套流程。判断标准就是:有没有第二个项目需要用它。

相关推荐
云上小朱2 小时前
开发测试环境-Kubernetes离线部署指南
后端
遨翔在知识的海洋里2 小时前
nestjs(1)-模块的相互调用
后端
Profile排查笔记2 小时前
指纹浏览器推荐:用一套验收清单筛选 Profile、代理与自动化能力
前端·人工智能·后端·自动化
jsl_jsl_jsl2 小时前
JUC速记
后端
摇滚侠2 小时前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 使用 JdbcTemplate 阅读笔记 31
android·spring boot·笔记
站大爷IP2 小时前
Python的pip依赖把我折腾惨了,原来requirements.txt和poetry能打出火星撞地球
后端
torpidcat2 小时前
ruoyi-vue-pro 若依芋道 java springboot +mybatis 生日查询相关
java·vue.js·spring boot
狗哥哥2 小时前
用“十步学习法”带你学会事件驱动架构
后端
AC赳赳老秦2 小时前
文旅市场公开数据分析:基于 OpenClaw 采集景区客流与门票公示数据,生成区域文旅热度监测报告
java·c语言·python·php·symfony·deepseek·openclaw