系列文章第2篇 | 共5篇
难度:⭐⭐ | 适合人群:想动手实践的开发者
📝 上期回顾
上一篇我们学到了:
- ✅ Starter是SpringBoot的"套餐"机制
- ✅ Starter能告别配置地狱,统一依赖管理
- ✅ 官方和第三方Starter的命名规范
上期思考题解答:
Q1: Starter是怎么做到"自动配置"的?
A: 通过spring.factories文件 + @Configuration配置类 + SpringBoot的自动装配机制。(本篇详细讲解)
Q2: 我能不能自己写一个Starter?
A: 当然可以!今天就手把手教你写!🎉
Q3: Starter的原理是什么?
A: 看完今天的实战,原理自然就懂了!
💭 开场:一个真实的需求
时间: 周一早会
地点: 会议室
技术经理: "咱们公司的10个微服务都需要统一的日志格式,每次都要复制一堆配置代码,太麻烦了。小李啊,能不能封装一个Starter,大家直接引用就能用?"
小李(我): "Starter?我只会用,不会写啊..." 😰
技术经理: "这周学一下,下周给我看成果。"
我: "...好吧。" (内心:完了,要现学现卖了)
一周后...
我: "经理,搞定了!我写了一个common-log-spring-boot-starter,大家只需要引入依赖,日志格式就自动统一了!"
技术经理: "不错啊!怎么实现的?"
我: "其实很简单,就四步..." 😎
🎯 本篇目标
今天我们要实现一个超简单但完整的Starter:
功能: 提供一个HelloService,自动注入到Spring容器
效果: 其他项目引入依赖后,直接使用HelloService
虽然简单,但麻雀虽小五脏俱全,包含了Starter的所有核心要素!
🛠️ 第一步:创建SpringBoot项目
Q:为什么Starter本身也是SpringBoot项目?
A: 因为Starter需要用到SpringBoot的自动配置机制啊!
类比: 就像你要做面包,得先有面粉(SpringBoot)一样。
创建项目
方式一:使用IDEA创建(推荐)
- File → New → Project
- 选择 Spring Initializr
- 填写项目信息:
Group: com.example Artifact: hello-spring-boot-starter Name: hello-spring-boot-starter Package name: com.example.starter - Dependencies: 暂时不选,后面手动加
- 点击 Create
方式二:使用 Spring Initializr 网站
Project: Maven Project
Language: Java
Spring Boot: 2.7.x(选择稳定版本)
Group: com.example
Artifact: hello-spring-boot-starter
Packaging: Jar
Java: 8 或 11
点击 GENERATE,下载后导入IDEA
清理项目
创建完成后,项目结构是这样的:
hello-spring-boot-starter/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/starter/
│ │ │ └── HelloSpringBootStarterApplication.java ← 删掉
│ │ └── resources/
│ │ └── application.properties ← 删掉
│ └── test/ ← 整个删掉
├── pom.xml
└── ...
为啥要删?
HelloSpringBootStarterApplication:Starter不需要启动类application.properties:Starter不需要自己的配置文件test目录:为了简洁,测试放在使用方项目
删除后的目录结构:
hello-spring-boot-starter/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/starter/ ← 后面在这里写代码
│ └── resources/
│ └── META-INF/ ← 后面在这里创建
└── pom.xml
看起来清爽多了! ✨
📦 第二步:配置pom.xml
完整的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 项目基本信息 -->
<groupId>com.example</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>hello-spring-boot-starter</name>
<description>我的第一个自定义Starter</description>
<!-- 继承SpringBoot父项目 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 核心依赖:SpringBoot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 可选:配置元数据(后面文章会讲) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 可选:Lombok,简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
Q:为啥只需要这几个依赖?
A: 因为Starter的核心就是"自动配置"!
依赖分析:
| 依赖 | 作用 | 必需? |
|---|---|---|
spring-boot-starter | 提供SpringBoot核心功能 | ✅ 必需 |
spring-boot-configuration-processor | 生成配置元数据(智能提示) | ⭕ 可选 |
lombok | 简化代码(@Data等) | ⭕ 可选 |
记住: Starter的pom.xml要保持简洁,不要引入业务相关的依赖!
Q:为什么有些依赖标记了<optional>true</optional>?
A: 这是个好问题!
场景演示:
<!-- Starter项目 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional> ← 注意这里
</dependency>
效果:
- Starter项目自己: 可以用Lombok
- 引用Starter的项目: 不会自动引入Lombok
为什么这样设计?
lombok、configuration-processor是开发时工具- 使用方不需要这些依赖
- 避免依赖污染
记忆技巧: 工具类依赖加optional=true,业务类依赖不加!
💻 第三步:编写核心代码
3.1 创建Service类
在src/main/java/com/example/starter/下创建HelloService.java:
package com.example.starter;
/**
* Hello服务类
* 提供简单的问候功能
*/
public class HelloService {
private String prefix; // 前缀
private String suffix; // 后缀
public HelloService(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
}
/**
* 说Hello
* @param name 名字
* @return 问候语
*/
public String sayHello(String name) {
return prefix + " " + name + " " + suffix;
}
}
代码解读:
- 非常简单的一个Service类
- 接收前缀和后缀参数
- 提供
sayHello方法拼接字符串
Q:为啥不加@Service注解?
A: 因为这个类要在自动配置类中手动注册成Bean,不需要@Service!
3.2 创建配置属性类(可选但推荐)
在src/main/java/com/example/starter/下创建HelloProperties.java:
package com.example.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Hello配置属性类
* 对应配置文件中的 hello.* 配置
*/
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
/**
* 前缀,默认值:Hello
*/
private String prefix = "Hello";
/**
* 后缀,默认值:!
*/
private String suffix = "!";
// Getter 和 Setter
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
代码解读:
@ConfigurationProperties(prefix = "hello")
↓
意思是:读取配置文件中以 hello. 开头的配置
配置文件中这样写:
hello.prefix=你好
hello.suffix=!
就会自动映射到这个类的属性上!
Q:不写这个类行不行?
A: 行,但是不推荐!
对比:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 有Properties类 | 配置集中管理 类型安全 支持默认值 IDE有提示 | 多写一个类 |
| 没有Properties类 | 少写代码 | 配置散乱 容易出错 没有提示 |
结论: 多写几行代码,换来更好的体验,值!✅
3.3 创建自动配置类(核心!)
在src/main/java/com/example/starter/下创建HelloAutoConfiguration.java:
package com.example.starter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Hello自动配置类
* SpringBoot启动时会自动加载这个配置类
*/
@Configuration // ← 标记为配置类
@EnableConfigurationProperties(HelloProperties.class) // ← 启用配置属性
public class HelloAutoConfiguration {
/**
* 注册HelloService到Spring容器
* @param properties 配置属性
* @return HelloService实例
*/
@Bean // ← 注册为Bean
@ConditionalOnMissingBean // ← 如果容器中没有HelloService才注册
public HelloService helloService(HelloProperties properties) {
return new HelloService(properties.getPrefix(), properties.getSuffix());
}
}
代码深度解读:
注解1:@Configuration
@Configuration
public class HelloAutoConfiguration {
作用: 告诉Spring这是一个配置类,类似于XML配置文件
效果: Spring会扫描这个类中的@Bean方法
注解2:@EnableConfigurationProperties
@EnableConfigurationProperties(HelloProperties.class)
作用: 启用HelloProperties配置类
效果:
- 将
HelloProperties注册为Bean - 自动读取配置文件中的
hello.*配置 - 注入到
HelloProperties对象中
Q:不加这个注解会怎样?
A: HelloProperties不会生效,配置文件的值读取不到!
注解3:@Bean
@Bean
public HelloService helloService(HelloProperties properties) {
return new HelloService(properties.getPrefix(), properties.getSuffix());
}
作用: 将方法返回值注册为Spring Bean
效果: 其他地方可以通过@Autowired注入HelloService
注解4:@ConditionalOnMissingBean
@ConditionalOnMissingBean
public HelloService helloService(HelloProperties properties) {
作用: 条件注册——只有容器中没有HelloService时才注册
效果: 允许使用方自定义HelloService覆盖默认实现
场景演示:
// 使用方项目中
@Configuration
public class CustomConfig {
@Bean
public HelloService helloService() {
return new HelloService("自定义前缀", "自定义后缀");
}
}
// 因为有了@ConditionalOnMissingBean
// Starter就不会再注册HelloService
// 使用的是用户自定义的版本!
这就是"约定大于配置,但允许覆盖"的体现! 💡
代码结构总结
到这里,我们的代码结构是这样的:
src/main/java/com/example/starter/
├── HelloService.java ← 业务类
├── HelloProperties.java ← 配置属性类
└── HelloAutoConfiguration.java ← 自动配置类(核心)
三个类的关系:
HelloProperties(配置)
↓ 读取配置文件
HelloAutoConfiguration(自动配置)
↓ 创建并注册Bean
HelloService(业务类)
↓ 被注入到Spring容器
使用方可以 @Autowired 使用
📄 第四步:创建spring.factories(关键!)
Q:什么是spring.factories?
A: 它是SpringBoot的"插件注册表"!
类比:
- 你的Starter是一个"插件"
spring.factories是"插件商店的登记表"- SpringBoot启动时会查这个表,找到你的插件并加载
创建文件
在src/main/resources/下创建目录和文件:
src/main/resources/
└── META-INF/
└── spring.factories ← 创建这个文件
注意:
- 目录名必须是
META-INF(全大写) - 文件名必须是
spring.factories - 位置不能错,否则SpringBoot扫描不到!
文件内容
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.HelloAutoConfiguration
格式解读:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
↓
这是固定的key,SpringBoot会读取这个key
com.example.starter.HelloAutoConfiguration
↓
你的自动配置类的全限定名
注意事项:
-
等号后面的
\是什么?- 换行符,如果配置很多可以换行写
- 如果只有一个配置类,可以不要
\
-
可以配置多个吗?
- 可以!用逗号分隔:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.starter.HelloAutoConfiguration,\ com.example.starter.OtherAutoConfiguration -
文件编码是什么?
- UTF-8或ISO-8859-1都可以
- IDEA默认会用正确的编码
Q:为什么要指定META-INF目录?
A: 这是SpringBoot的约定!
原理简述:
// SpringBoot源码(简化版)
public class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION =
"META-INF/spring.factories"; // ← 看!写死的路径
public static List<String> loadFactoryNames() {
// 扫描所有jar包的 META-INF/spring.factories
// 读取配置
// 加载自动配置类
}
}
你不按这个路径写?SpringBoot: "对不起,扫不到!" 🙅
📦 第五步:打包到本地仓库
Q:为什么要打包?
A: 因为要给其他项目用啊!
类比:
- 你写的Starter是一个"商品"
- 打包就是"装箱"
- 本地仓库就是"自己的仓库"
- 其他项目就是"客户"
Maven三大打包命令
命令对比:
| 命令 | 全称 | 作用 | 结果 |
|---|---|---|---|
package | 打包 | 编译+测试+打jar包 | jar包在target目录 |
install | 安装 | package + 部署到本地仓库 | jar包在本地Maven仓库 |
deploy | 部署 | install + 部署到远程仓库 | jar包在本地+远程仓库 |
我们用哪个? install ✅
为什么?
package:只打包,不部署,其他项目引用不到install:部署到本地仓库,本机其他项目能用,完美!deploy:需要配置远程仓库,现在用不上
打包方式一:IDEA Maven插件
步骤:
- 打开右侧 Maven 面板
- 展开项目 → Lifecycle
- 双击
clean(清理旧文件) - 双击
install(打包安装)
看到这个就成功了:
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.141 s
[INFO] Finished at: 2024-01-15T10:30:00+08:00
[INFO] ------------------------------------------------------------------------
打包方式二:命令行(推荐)
步骤:
- 打开项目根目录(pom.xml所在目录)
- 打开终端/命令行
- 执行命令:
mvn clean install
参数说明:
clean:清理target目录install:编译、测试、打包、安装到本地仓库
跳过测试(可选):
mvn clean install -DskipTests
验证是否打包成功
查看本地仓库:
Windows: C:\Users\你的用户名\.m2\repository\com\example\hello-spring-boot-starter\1.0.0\
Mac/Linux: ~/.m2/repository/com/example/hello-spring-boot-starter/1.0.0/
应该看到这些文件:
hello-spring-boot-starter-1.0.0.jar ← Starter的jar包
hello-spring-boot-starter-1.0.0.pom ← pom文件
hello-spring-boot-starter-1.0.0.jar.sha1 ← 校验文件
看到了?恭喜,打包成功! 🎉
🧪 第六步:创建测试项目验证
创建测试项目
方式: 创建一个普通的SpringBoot项目
Group: com.example
Artifact: starter-test
Dependencies: Spring Web(方便测试)
引入自定义Starter
在测试项目的pom.xml中添加:
<dependencies>
<!-- 引入我们自己写的Starter -->
<dependency>
<groupId>com.example</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Spring Web(用于测试) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
配置文件(可选)
在application.yml中配置:
# 使用默认配置(不写这个也行)
hello:
prefix: Hello
suffix: !
# 或者自定义配置
hello:
prefix: 你好
suffix: ,欢迎你!
编写测试Controller
package com.example.test.controller;
import com.example.starter.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired // ← 直接注入,不需要任何配置!
private HelloService helloService;
@GetMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return helloService.sayHello(name);
}
}
代码亮点:
- ✨ 没有写任何配置类
- ✨ 直接
@Autowired就能用 - ✨ 这就是Starter的魅力!
运行测试
启动项目:
@SpringBootApplication
public class StarterTestApplication {
public static void main(String[] args) {
SpringApplication.run(StarterTestApplication.class, args);
}
}
访问接口:
http://localhost:8080/hello/World
预期结果(默认配置):
Hello World !
预期结果(自定义配置):
hello:
prefix: 你好
suffix: ,欢迎你!
你好 World ,欢迎你!
看到这个结果?恭喜你,Starter写成功了! 🎉🎉🎉
🎯 测试场景汇总
场景1:默认配置
配置文件: 不写任何配置
结果:
访问:/hello/张三
输出:Hello 张三 !
结论: 默认配置生效 ✅
场景2:自定义配置
配置文件:
hello:
prefix: 早上好
suffix: ,祝你开心!
结果:
访问:/hello/李四
输出:早上好 李四 ,祝你开心!
结论: 自定义配置生效 ✅
场景3:覆盖默认Bean
测试项目中自定义Bean:
@Configuration
public class CustomConfig {
@Bean
public HelloService helloService() {
return new HelloService("自定义", "覆盖成功");
}
}
结果:
访问:/hello/王五
输出:自定义 王五 覆盖成功
结论: @ConditionalOnMissingBean生效,用户可以覆盖默认实现 ✅
🐛 常见问题排查
问题1:注入失败
现象:
@Autowired
private HelloService helloService; // ← 报错:Could not autowire
原因排查:
-
检查spring.factories是否创建
- 位置:
src/main/resources/META-INF/spring.factories - 路径对不对?
- 位置:
-
检查spring.factories内容
# 类的全限定名对不对? org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.starter.HelloAutoConfiguration -
检查是否重新打包
- 修改代码后必须重新
mvn clean install
- 修改代码后必须重新
-
检查依赖是否引入
<dependency> <groupId>com.example</groupId> <artifactId>hello-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>
问题2:配置不生效
现象:
hello:
prefix: 你好
但输出还是: Hello 张三 !
原因排查:
-
检查@EnableConfigurationProperties
@Configuration @EnableConfigurationProperties(HelloProperties.class) // ← 有这个吗? public class HelloAutoConfiguration { -
检查配置文件格式
# ❌ 错误:多了空格 hello : prefix: 你好 # ✅ 正确 hello: prefix: 你好 -
检查配置属性前缀
@ConfigurationProperties(prefix = "hello") // ← 前缀对吗? public class HelloProperties {
问题3:打包后找不到jar
现象: 执行mvn install后,其他项目引用报错
原因排查:
-
检查打包是否成功
- 看控制台有没有
BUILD SUCCESS
- 看控制台有没有
-
检查本地仓库
- 去
~/.m2/repository/看jar包在不在
- 去
-
检查坐标是否一致
<!-- Starter的pom.xml --> <groupId>com.example</groupId> <artifactId>hello-spring-boot-starter</artifactId> <version>1.0.0</version> <!-- 测试项目的pom.xml --> <dependency> <groupId>com.example</groupId> ← 一致吗? <artifactId>hello-spring-boot-starter</artifactId> ← 一致吗? <version>1.0.0</version> ← 一致吗? </dependency>
💡 知识点总结
本篇你学到了什么?
✅ Starter的四大核心组件
HelloService:业务类HelloProperties:配置属性类HelloAutoConfiguration:自动配置类spring.factories:注册文件
✅ 关键注解的作用
@Configuration:标记配置类@EnableConfigurationProperties:启用配置属性@Bean:注册Bean@ConditionalOnMissingBean:条件注册
✅ spring.factories的作用
- SpringBoot的"插件注册表"
- 必须放在
META-INF目录 - 告诉SpringBoot加载哪些自动配置类
✅ 打包部署流程
mvn clean install打包到本地仓库- 其他项目通过坐标引用
🎓 进阶思考
思考1:如果业务类很多怎么办?
场景: 不只一个HelloService,还有GoodbyeService、WelcomeService...
解决方案:
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public HelloService helloService(HelloProperties properties) {
return new HelloService(properties.getPrefix(), properties.getSuffix());
}
@Bean
@ConditionalOnMissingBean
public GoodbyeService goodbyeService() {
return new GoodbyeService();
}
@Bean
@ConditionalOnMissingBean
public WelcomeService welcomeService() {
return new WelcomeService();
}
}
或者分多个配置类:
# spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.HelloAutoConfiguration,\
com.example.starter.GoodbyeAutoConfiguration,\
com.example.starter.WelcomeAutoConfiguration
思考2:如果想让Starter可插拔怎么办?
什么是可插拔?
- 引入了依赖,但可以选择不启用
解决方案: 下一篇文章详细讲!敬请期待! 😎
🤔 留个思考题
问题: 如果我想让HelloService只在特定条件下才注册(比如配置文件中hello.enabled=true时才生效),应该怎么做?
提示: 关键词@ConditionalOnProperty
答案: 下一篇揭晓!
📢 下期预告
《手写SpringBoot Starter(三):实现可插拔Starter,像Zuul一样优雅!》
下一篇我们将:
- 理解什么是"可插拔"
- 学习
@Conditional系列注解 - 实现三种可插拔方式
- 分析Zuul、MyBatis的可插拔实现
- 对比不同方案的优劣
让你的Starter更加灵活和专业! 🚀
💬 互动时间
你在写Starter时遇到过什么问题?
想实现什么样的Starter?
对哪部分内容还有疑问?
欢迎在评论区分享!我会一一回复!💭
觉得有帮助?别忘了三连支持: 👍 点赞 | ⭐ 收藏 | 🔄 转发
跟着这篇文章,你已经成功写出了第一个Starter! 🎉
下一篇见! 👋