本文是视频《手把手教你创建自己的 Spring Boot Starter(Dan Vega 教程)》(作者:YouTube 原作者)的精读笔记,配套中配视频观看效果更佳。
手把手教你创建自己的 Spring Boot Starter(Dan Vega 教程)
做过 Spring Boot 的人几乎都用过 starter,但很少有人反向拆开看过它。这条约 26 分钟的视频里,Dan Vega 从 Spring 官方文档的命名规则讲起,用一个"JSON Placeholder 客户端"的完整例子,把 starter 从零建到装进另一个应用跑通。值得看的地方在于:他保留了所有踩坑的瞬间——主类该不该删、自动配置怎么被找到、没有启动类怎么测试。
图:26 分钟的完整链路,终点是新应用里一次 GET /todos 返回的 200 条待办
全景:一条主线看懂
全片只回答一个问题:怎么把"能复用的能力"打包成别人 import 一行就能用的东西。主线是——先确认它该叫什么名字,再把它从"应用"改造成"库",接着把可变的配置外置、把稳定的逻辑固化成 Bean,然后解决它如何被 Spring 自动发现、如何被测试,最后 mvn clean install,在另一个应用里验证它真的活着。
图:命名 → 瘦身 → 配置类 → 注册自动配置 → 测试 → 安装与消费,六步闭环
一、先读懂 starter 的名字:它决定了谁能用你的作品
Dan 的第一件事不是打开 IDE,而是去翻官方文档里专门讲 starter 的那一节。理由很实在:这类偏底层的知识,网上零散资料少,出错了你总得有个权威地方回头看。
文档里那节叫"名字里有什么"(What is in a name),讲的是命名规范——所有官方 starter 都遵循同一套规则:
| 归属 | 命名格式 | 示例 |
|---|---|---|
| Spring 官方 | spring-boot-starter-<应用类型> | spring-boot-starter-web |
| 第三方/自建 | <项目名>-spring-boot-starter | jps-spring-boot-starter |
这条规则不只是形式。spring-boot-starter- 是官方保留的命名空间,所以 Dan 直说:你不能把自己的东西叫 spring-boot-starter-Dan。反过来,看到 xxx-spring-boot-starter,你就能判断这是社区或第三方项目做的。
他还顺带点了一个容易忽略的知识点:你能直接依赖的"核心 starter"只有 spring-boot-starter,里面装着自动配置支持、日志和 YAML 解析。而一个 starter 真正的价值,是把传递依赖和自动配置一起打包——加一个 web starter,你不需要自己去凑那些子依赖,项目立刻能 bootstrap 起来。
文档另外还讲了三件当时没展开但后面全用上了的事:配置键(configuration keys)、自动配置模块该做什么、以及文件放在哪里才会被读取。它还提到组织方式可以是一个项目,也可以是"自动配置一个项目、starter 一个项目"的两段式。视频里为了讲清楚,选了单项目的最简路径。
本节要点:starter 的本质 = 传递依赖 + 自动配置;名字不是随便取的,官方用
spring-boot-starter-*,第三方用*-spring-boot-starter,先分清自己站在哪一边。
二、把一个 Spring Boot 项目改造成"库"
项目从 start.spring.io 生成,Java 项目,Spring Boot 3.2.2,groupId 用 dev.danvega.jps(jps 就是 JSON Placeholder Service 的缩写)。依赖只勾了一个 web——理由不是要写 Web 页面,而是 web 包里带着 RestClient,用它去调用外部服务、序列化和反序列化返回的数据。
生成、下载、用 IDE 打开之后,Dan 开始做减法,这部分是全片最有"反直觉"感的地方:
- 删掉主应用类。他给的理由是核心判断:starter 不是应用,所以那个
main方法不需要存在。 - 删掉
application.properties。starter 自己不从属性文件里读东西,属性由使用它的应用来填。 - 依赖里只留
spring-boot-starter-web,其他全部清掉。 - 删掉 Spring Boot Maven 插件,也删掉测试样板。他把这些统称为"基础设施的管道"(infrastructure plumbing)——它们服务于打包成可执行应用,而这里要做的是被人依赖的库。
图:删主类、删 properties、删插件,一步步把一个"应用"还原成"库"
这一步的思维转换值得单独记下来:同一个构建文件,做应用时留的东西,做库时反而是噪音。
本节要点:starter 不是应用。删掉主类、属性文件和打包插件,只留下它真正需要的依赖(这里就是带 RestClient 的 web),是从"写程序"切换到"做组件"的第一步。
三、把可变的留在外面,把必要的给成默认值
接下来是真正写代码的部分,Dan 建了两个东西。
第一个是属性类,命名叫 JSON Placeholder Service Properties。他提到一个习惯:用 @ConfigurationProperties 时最好有个命名约定,他习惯以 Properties 结尾(当然你也可以另取)。类上声明配置前缀,内部用 record 而不是 class,record 里只有一个字段——base URL。
这个 base URL 有个细节很有代表性:Dan 给它加了默认值注解,默认指向 JSON Placeholder 的公网地址。动机来自一次真实翻车——Wi-Fi 不好使的时候他慌了,因为演示要用的服务没了。好消息是 JSON Placeholder 的代码可以下载到本地跑,改一下 URL 就行。所以默认值给公网地址,谁想覆盖就在自己的应用里改。
第二个是自动配置类。这里他卡过一次:属性类上出现了红色波浪线,因为通常 @EnableConfigurationProperties 会挂在主应用类上,而这个项目没有主应用类。于是这个注解落到配置类上,配合自动配置注解,让属性类被启用。配置类还需要一个 logger、需要拿到属性(走构造函数注入),然后它的职责就是:把应用里需要的一切 Bean 都配出来。
在这个类里他定义了两个 Bean:
- 一个
RestClient,从RestClient.Builder出发,把 baseUrl 设成属性里的值再build(); - 一个 JPS ToDoClient,构造函数接收上面那个 RestClient。他还差点忘了给这个方法加
@Bean,当场补上。
图:属性 record 提供默认 baseUrl,配置类负责组装 RestClient 与 ToDoClient
顺带一提,JSON Placeholder 提供待办、用户、评论、照片、相册这几组端点,视频里只做了待办,其余留着以后回来加。这不是偷懒,恰恰是 starter 的用法:先让它在一个场景上闭环。
本节要点:可变的东西(base URL)做成配置属性并给默认值,稳定的东西(客户端、REST 调用能力)做成 Bean。属性能被覆盖,是 starter 好用与否的分水岭。
四、没有主应用类,Spring 靠什么找到你的配置
代码写到这里会报错,Dan 的困惑说得很形象:
"我在一个应用里,但又不在一个应用里,对吧?没有主应用类。"——Dan 在配置类出现编译错误、意识到缺少启动入口时说的
那自动配置怎么运行?答案是约定——而且这个约定在新版 Spring Boot 里已经换过了。旧版本用 META-INF/spring.factories 注册自动配置类;现在要新建 resources/META-INF/spring/ 目录,在里面创建一个名为 org.springframework.boot.autoconfigure.AutoConfiguration.imports 的文件,然后把配置类的完整全限定类名写进去。
| 版本 | 注册文件 | 位置 |
|---|---|---|
| Spring Boot 2.x 及之前 | spring.factories | META-INF/spring.factories |
| Spring Boot 3.x(本视频) | org.springframework.boot.autoconfigure.AutoConfiguration.imports | META-INF/spring/ |
图:一行全限定类名,就是你的 starter 被 Spring 认领的全部凭证
写完 Maven reload,红色波浪线全部消失,再跑一次 build 也没有错误。Dan 说这是"一切看起来正常"的时刻——一个没有任何运行入口的项目,静悄悄地合法了。
本节要点:Spring Boot 3.x 用
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports注册自动配置类,取代了旧版的spring.factories。没有主类也能被加载,靠的就是这个文件。
五、怎么测试一个"不是应用"的东西
接下来的问题更现实:没有主应用类,没法手动跑起来"踢踢轮胎",怎么验证它是对的?Dan 坦言这点也让他困惑过,于是他进客户端类旁边建了测试。
工具是 ApplicationContextRunner:先 new 一个运行器,再用 withConfiguration(...) 指定要加载的配置。这里要传两个——自己写的 JSON Placeholder 服务配置,以及 RestClientAutoConfiguration。第二个不能漏:RestClient.Builder 是个接口,平时由 Spring 的自动配置提供默认实现,而现在没有应用启动过程,不显式引入就会"一堆问题"。
有了上下文之后,断言两件事就够说明自动配置生效了:上下文中存在那个 REST 客户端 Bean(他为它起了显式名字,避免和默认名冲突),以及存在 ToDo 客户端 Bean。
通过 = 自动配置类被 import 文件加载 → Bean 被放进应用上下文测试还覆盖了两个业务目标:默认 base URL 是否存在;如果使用者覆盖了 base URL,这个覆盖是不是"sticky"(保持生效、不被回退)。
图:不用启动应用,也能断言 Bean 已经装配进上下文
之后 Dan 往客户端里补了 findAll、findOne、create 这类常规方法,并让测试真的去访问服务:验证拿到 200 条待办,再验证单条查询也正常。他强调客户端的实现本身不是本节重点,重点是这种测试写法能跑通。
本节要点:没有主类就用
ApplicationContextRunner组装上下文,用withConfiguration引入自己的配置 +RestClientAutoConfiguration,断言 Bean 存在,再断言默认值和覆盖值的行为。
六、装进本地仓库,然后拿到真实应用里验证
代码通过测试后,还要证明它真的能被"用"。
Dan 打开终端,在项目里执行 mvn clean install,把构件装进本地 Maven 仓库。然后他 cd 进 ~/.m2/repository/dev/danvega,先确认里面原本没有自己的 starter,构建后出现了 JPS 目录,版本是 0.0.1-SNAPSHOT。他还提到一个实用细节:改了版本号再跑一次,仓库里就会多出第二个版本,老版本仍然在。
| 项 | 值 |
|---|---|
| groupId | dev.danvega.jps |
| Spring Boot 版本 | 3.2.2 |
| 唯一直接依赖 | spring-boot-starter-web |
| 产物版本 | 0.0.1-SNAPSHOT |
| 演示应用 | HelloJPS,请求 localhost:8080/todos |
接着他新建了一个 Spring Boot 应用 HelloJPS,在依赖块里加上自己的 starter,刷新 Maven(他笑称要"祈祷一下"),没有报错,展开依赖能看到 starter 的代码就在里面。
图:mvn clean install 之后,本地仓库里多出了自己的 0.0.1-SNAPSHOT
消费方式也很日常:写一个 REST controller,加上请求路径映射,通过构造函数注入 JPS ToDoClient,调用 findAll。启动应用,用命令行工具 HTTPie 请求 localhost:8080/todos,返回 200 条待办——这 200 条数据就是 starter 生效的证据。
图:新应用里只加了一行依赖,/todos 就返回 200 条待办
最后 Dan 划了边界,也给了更大的想象空间:
- 这是非常简单的例子,不是最好的做法,也不是最高级的做法,"先学会走,再学跑"。
"我们得先学会走,再学会跑。"——Dan 在结尾总结这个例子只是入门级做法时说的
- 开头提过的另一种组织方式是把配置和代码拆成两个项目,这里为了简单放在了一起。
- 本教程不涉及发布到 Maven 中央仓库,那会另做一期。
- 更有价值的场景是:他经常把 Notion 接到 Spring 应用里,每次都要复制粘贴代码或重新研究一遍怎么做——这正是该做 starter 的地方。
- 另一个场景在公司内部:组织有通用的做事方式,把标准做法固化成一个 starter,比靠文档和口口相传可靠得多。
本节要点:安装到本地仓库、在另一个应用里引入、请求真实端点拿到数据,才算完整闭环。starter 的真正用武之地是"重复的手工集成"和"组织内部的统一约定"。
总结要点
- starter = 传递依赖 + 自动配置。它的价值是让别人加一行依赖就能跑起来,而不是省下几行 import。
- 命名先分清阵营:官方是
spring-boot-starter-<类型>,自建是<项目名>-spring-boot-starter,别去占用官方命名空间。 - 做库和做应用是两套姿势:删主类、删 properties、删打包插件,只留真正需要的能力依赖。
- Spring Boot 3.x 的自动配置注册文件换了:从
spring.factories变成META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,里面写全限定类名。 - 没有主类也能测:
ApplicationContextRunner+withConfiguration,断言 Bean 存在,再验证默认值与覆盖值;最后mvn clean install到另一个应用里做一次真实请求才算完。
迁移与启发
- 把"每次都重抄一遍的东西"当成资产,而不是琐事。Dan 反复手动把 Notion 接到 Spring 应用的痛苦,和我们在职场里每次都从零写一份周报模板、重新推导一遍报销规则没有本质区别。花一次时间把它固化成"加一行就能用"的组件或清单,之后每一次复用都在回收成本。
- 默认值 + 允许覆盖,是设计任何可复用方案的最低标准。starter 里 base URL 给公网默认值、又允许使用者改成本地地址,对应到生活决策就是:给一个别人能直接用的方案,同时留出他按自己情况调整的接口。只会"必须照我说的做"的方案,复用率一定低。
- 先在一个场景闭环,再谈扩展。视频只做了待办,用户、评论、照片、相册都留着以后加。理财里的定投、工作里的流程改造也一样:先在一个账户、一个环节跑通并拿到可验证的结果(比如"200 条数据返回成功"),比一次性设计大而全的体系更容易活下来。
一句话升华
真正让能力被复用的,从来不是写得多聪明,而是别人加一行依赖就能用起来——先学会走,再学跑。
