Skip to content

本文是视频《手把手教你创建自己的 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-starterjps-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.factoriesMETA-INF/spring.factories
Spring Boot 3.x(本视频)org.springframework.boot.autoconfigure.AutoConfiguration.importsMETA-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。

text
通过 = 自动配置类被 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。他还提到一个实用细节:改了版本号再跑一次,仓库里就会多出第二个版本,老版本仍然在。

项值
groupIddev.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 的真正用武之地是"重复的手工集成"和"组织内部的统一约定"。

总结要点 ​

  1. starter = 传递依赖 + 自动配置。它的价值是让别人加一行依赖就能跑起来,而不是省下几行 import。
  2. 命名先分清阵营:官方是 spring-boot-starter-<类型>,自建是 <项目名>-spring-boot-starter,别去占用官方命名空间。
  3. 做库和做应用是两套姿势:删主类、删 properties、删打包插件,只留真正需要的能力依赖。
  4. Spring Boot 3.x 的自动配置注册文件换了:从 spring.factories 变成 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,里面写全限定类名。
  5. 没有主类也能测:ApplicationContextRunner + withConfiguration,断言 Bean 存在,再验证默认值与覆盖值;最后 mvn clean install 到另一个应用里做一次真实请求才算完。

迁移与启发 ​

  1. 把"每次都重抄一遍的东西"当成资产,而不是琐事。Dan 反复手动把 Notion 接到 Spring 应用的痛苦,和我们在职场里每次都从零写一份周报模板、重新推导一遍报销规则没有本质区别。花一次时间把它固化成"加一行就能用"的组件或清单,之后每一次复用都在回收成本。
  2. 默认值 + 允许覆盖,是设计任何可复用方案的最低标准。starter 里 base URL 给公网默认值、又允许使用者改成本地地址,对应到生活决策就是:给一个别人能直接用的方案,同时留出他按自己情况调整的接口。只会"必须照我说的做"的方案,复用率一定低。
  3. 先在一个场景闭环,再谈扩展。视频只做了待办,用户、评论、照片、相册都留着以后加。理财里的定投、工作里的流程改造也一样:先在一个账户、一个环节跑通并拿到可验证的结果(比如"200 条数据返回成功"),比一次性设计大而全的体系更容易活下来。

一句话升华 ​

真正让能力被复用的,从来不是写得多聪明,而是别人加一行依赖就能用起来——先学会走,再学跑。