系列文章第4篇 | 共5篇
难度:⭐⭐⭐ | 适合人群:想做企业级Starter的架构师
📝 上期回顾
上一篇我们实现了可插拔Starter:
- ✅ 学习了三种可插拔实现方式
- ✅ 深度分析了Zuul的实现原理
- ✅ 改造了自己的Starter支持
@EnableHello
上期思考题解答:
Q: 如何实现更复杂的条件(如只在生产环境启用)?
A: 使用@ConditionalOnXxx家族注解,本篇会涉及!
💥 开场:一次"专业度"的拷问
时间: 周五下午
地点: 技术评审会
技术总监: "小李啊,你写的这个Starter用起来还不错。"
我: "谢谢总监!" 😊(内心窃喜)
技术总监: "不过有个问题..."(打开IDEA演示)
总监操作:
# 输入 spring. 后,IDE会自动提示
spring:
redis: # ← 有提示、有说明、有默认值
host: localhost
port: 6379
然后输入:
# 输入 hello. 后...
hello:
# ← 啥都没有!没提示!没说明!没默认值!
技术总监: "看到了吗?官方的配置都有智能提示,你这个啥都没有,用起来很不方便啊。"
我: "这..." 😰(尴尬)
技术总监: "下周改进一下,让它也能有智能提示。"
我: "好的..." (内心:咋整?)
回到工位,开始研究...
我: "为啥官方的有提示,我的没有?" 🤔
搜索一番后: "原来是因为缺少配置元数据!"
🤔 第一问:什么是配置元数据?
Q1:先看现象
官方Starter的配置体验:
spring:
redis:
# 输入时IDE会提示:
# - host: Redis服务器地址 (默认: localhost)
# - port: Redis服务器端口 (默认: 6379)
# - password: Redis密码 (默认: 无)
# - database: 数据库索引 (默认: 0)
host: localhost
port: 6379
特点:
- ✅ 有智能提示
- ✅ 有字段说明
- ✅ 有默认值显示
- ✅ 有类型检查
我们的Starter:
hello:
# 啥都没有,全靠记忆
prefix: Hello
suffix: !
特点:
- ❌ 没有智能提示
- ❌ 不知道有哪些配置项
- ❌ 不知道默认值是什么
- ❌ 容易拼错字段名
差距太明显了! 😓
Q2:配置元数据到底是啥?
官方定义:
Configuration metadata 是用于描述配置属性的元数据文件,让IDE能够提供智能提示。
人话版本:
想象你去餐厅点菜:
- 没有菜单(没有元数据)= 只能问服务员有啥菜,全靠记忆
- 有菜单(有元数据)= 菜名、价格、描述、图片一目了然
配置元数据就是配置的"菜单"!
Q3:元数据长啥样?
位置: META-INF/spring-configuration-metadata.json
内容示例: (Spring官方的Redis配置元数据)
{
"groups": [
{
"name": "spring.redis",
"type": "org.springframework.boot.autoconfigure.data.redis.RedisProperties",
"sourceType": "org.springframework.boot.autoconfigure.data.redis.RedisProperties"
}
],
"properties": [
{
"name": "spring.redis.host",
"type": "java.lang.String",
"description": "Redis服务器地址",
"defaultValue": "localhost"
},
{
"name": "spring.redis.port",
"type": "java.lang.Integer",
"description": "Redis服务器端口",
"defaultValue": 6379
},
{
"name": "spring.redis.password",
"type": "java.lang.String",
"description": "Redis服务器密码"
}
]
}
看完: "卧槽,这么复杂?要手写这玩意?" 😱
好消息: 不用手写!SpringBoot提供了自动生成工具!✨
🛠️ 第二问:如何生成配置元数据?
方式一:手写JSON(不推荐)
步骤:
-
创建文件:
src/main/resources/META-INF/spring-configuration-metadata.json -
手写JSON:
{
"groups": [
{
"name": "hello",
"type": "com.example.starter.HelloProperties"
}
],
"properties": [
{
"name": "hello.enabled",
"type": "java.lang.Boolean",
"description": "是否启用Hello功能",
"defaultValue": true
},
{
"name": "hello.prefix",
"type": "java.lang.String",
"description": "问候语前缀",
"defaultValue": "Hello"
},
{
"name": "hello.suffix",
"type": "java.lang.String",
"description": "问候语后缀",
"defaultValue": "!"
}
]
}
缺点:
- ❌ 手写容易出错
- ❌ 代码改了,JSON要同步改
- ❌ 维护成本高
- ❌ 容易遗漏字段
结论: 不推荐!(除非你是受虐狂)😭
方式二:自动生成(强烈推荐)
原理: 使用spring-boot-configuration-processor注解处理器,在编译时自动生成元数据文件。
优点:
- ✅ 自动生成,零出错
- ✅ 代码改了自动更新
- ✅ 支持JavaDoc注释
- ✅ 维护成本低
实现步骤:
步骤1:添加依赖
在Starter项目的pom.xml中添加:
<dependencies>
<!-- 核心依赖 -->
<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> <!-- ← 注意:optional=true -->
</dependency>
</dependencies>
Q:为什么要加<optional>true</optional>?
A: 因为这是编译时工具,使用方不需要这个依赖!
类比:
- 编译时:需要这个工具生成元数据
- 运行时:不需要这个工具
- 使用方:更不需要
加了optional=true,使用方引入Starter时不会传递这个依赖。
步骤2:改造Properties类
之前的代码:
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
private boolean enabled = true;
private String prefix = "Hello";
private String suffix = "!";
// getter/setter...
}
改造后(添加JavaDoc注释):
package com.example.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Hello配置属性
*
* @author YourName
* @since 1.0.0
*/
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
/**
* 是否启用Hello功能
* <p>默认值:true</p>
*/
private boolean enabled = true;
/**
* 问候语前缀
* <p>默认值:Hello</p>
* <p>示例:你好、Hi、Welcome</p>
*/
private String prefix = "Hello";
/**
* 问候语后缀
* <p>默认值:!</p>
* <p>示例:!、~、...</p>
*/
private String suffix = "!";
/**
* 最大长度限制(可选配置)
* <p>默认值:100</p>
*/
private int maxLength = 100;
// Getter 和 Setter
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
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;
}
public int getMaxLength() {
return maxLength;
}
public void setMaxLength(int maxLength) {
this.maxLength = maxLength;
}
}
关键点:
- 类级别JavaDoc: 描述这个配置类的作用
- 字段级别JavaDoc: 描述每个配置项
- 第一行:简短描述
<p>默认值:xxx</p>:说明默认值<p>示例:xxx</p>:提供使用示例
- 必须有getter/setter: 处理器需要通过它们识别属性
步骤3:打包生成
执行打包命令:
mvn clean compile
注意: 只需要compile,不需要install(生成后再install)
查看生成结果:
target/classes/META-INF/
└── spring-configuration-metadata.json ← 自动生成的文件
打开文件查看:
{
"groups": [
{
"name": "hello",
"type": "com.example.starter.HelloProperties",
"sourceType": "com.example.starter.HelloProperties"
}
],
"properties": [
{
"name": "hello.enabled",
"type": "java.lang.Boolean",
"description": "是否启用Hello功能 默认值:true",
"sourceType": "com.example.starter.HelloProperties",
"defaultValue": true
},
{
"name": "hello.prefix",
"type": "java.lang.String",
"description": "问候语前缀 默认值:Hello 示例:你好、Hi、Welcome",
"sourceType": "com.example.starter.HelloProperties",
"defaultValue": "Hello"
},
{
"name": "hello.suffix",
"type": "java.lang.String",
"description": "问候语后缀 默认值:! 示例:!、~、...",
"sourceType": "com.example.starter.HelloProperties",
"defaultValue": "!"
},
{
"name": "hello.max-length",
"type": "java.lang.Integer",
"description": "最大长度限制(可选配置) 默认值:100",
"sourceType": "com.example.starter.HelloProperties",
"defaultValue": 100
}
],
"hints": []
}
看到了吗?自动生成,完美! 🎉
步骤4:打包发布
mvn clean install
验证: 去本地仓库检查jar包中是否包含元数据文件
# 解压jar包查看
jar -tf hello-spring-boot-starter-1.0.0.jar | grep metadata
# 应该输出:
# META-INF/spring-configuration-metadata.json
🧪 第三问:测试智能提示效果
步骤1:更新测试项目依赖
<dependency>
<groupId>com.example</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
如果之前引用过,需要:
- Maven → Reload Project
- 或者
mvn clean清理缓存
步骤2:测试智能提示
打开application.yml:
# 输入 hello. 然后按 Ctrl+Space(或自动弹出)
hello:
看到的效果:
智能提示列表:
┌─────────────────────────────────────────────────┐
│ ● hello.enabled │
│ 是否启用Hello功能 默认值:true │
│ │
│ ● hello.prefix │
│ 问候语前缀 默认值:Hello │
│ │
│ ● hello.suffix │
│ 问候语后缀 默认值:! │
│ │
│ ● hello.max-length │
│ 最大长度限制 默认值:100 │
└─────────────────────────────────────────────────┘
继续输入:
hello:
prefix: # 光标停在这里,IDE会显示:
# 问候语前缀 默认值:Hello
# 示例:你好、Hi、Welcome
感受到了吗?就像官方Starter一样专业! 😎
步骤3:测试类型检查
正确的配置:
hello:
max-length: 100 # ✅ 整数类型,正确
错误的配置:
hello:
max-length: abc # ❌ IDE会标红提示:期望整数类型
IDEA会直接提示类型错误!
🎨 第四问:进阶技巧
技巧1:嵌套配置
场景: 配置项很多,想要分组
实现:
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
/**
* 是否启用
*/
private boolean enabled = true;
/**
* 消息配置
*/
private Message message = new Message();
/**
* 限制配置
*/
private Limit limit = new Limit();
/**
* 消息相关配置
*/
public static class Message {
/**
* 前缀
*/
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;
}
}
/**
* 限制相关配置
*/
public static class Limit {
/**
* 最大长度
*/
private int maxLength = 100;
/**
* 最小长度
*/
private int minLength = 1;
// getter/setter...
public int getMaxLength() {
return maxLength;
}
public void setMaxLength(int maxLength) {
this.maxLength = maxLength;
}
public int getMinLength() {
return minLength;
}
public void setMinLength(int minLength) {
this.minLength = minLength;
}
}
// getter/setter...
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Message getMessage() {
return message;
}
public void setMessage(Message message) {
this.message = message;
}
public Limit getLimit() {
return limit;
}
public void setLimit(Limit limit) {
this.limit = limit;
}
}
配置文件使用:
hello:
enabled: true
message: # 消息组
prefix: 你好
suffix: !
limit: # 限制组
max-length: 200
min-length: 5
智能提示效果:
hello.
├─ enabled
├─ message.
│ ├─ prefix
│ └─ suffix
└─ limit.
├─ max-length
└─ min-length
优点: 配置分组,结构清晰!
技巧2:配置验证
场景: 希望配置值必须符合某些规则
实现: 使用JSR-303校验注解
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<optional>true</optional>
</dependency>
改造Properties:
package com.example.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.*;
@ConfigurationProperties(prefix = "hello")
@Validated // ← 开启校验
public class HelloProperties {
/**
* 问候语前缀
* 不能为空
*/
@NotBlank(message = "前缀不能为空")
private String prefix = "Hello";
/**
* 问候语后缀
* 不能为空
*/
@NotBlank(message = "后缀不能为空")
private String suffix = "!";
/**
* 最大长度
* 必须在1-1000之间
*/
@Min(value = 1, message = "最大长度不能小于1")
@Max(value = 1000, message = "最大长度不能大于1000")
private int maxLength = 100;
/**
* 超时时间(秒)
* 必须是正数
*/
@Positive(message = "超时时间必须大于0")
private int timeout = 30;
/**
* 邮箱地址(可选)
* 如果填写必须是合法邮箱
*/
@Email(message = "邮箱格式不正确")
private String email;
// 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;
}
public int getMaxLength() {
return maxLength;
}
public void setMaxLength(int maxLength) {
this.maxLength = maxLength;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
效果:
hello:
prefix: "" # ❌ 启动报错:前缀不能为空
max-length: 9999 # ❌ 启动报错:最大长度不能大于1000
timeout: -1 # ❌ 启动报错:超时时间必须大于0
email: abc # ❌ 启动报错:邮箱格式不正确
常用校验注解:
| 注解 | 作用 | 示例 |
|---|---|---|
@NotNull | 不能为null | @NotNull String name |
@NotBlank | 不能为空字符串 | @NotBlank String prefix |
@Min | 最小值 | @Min(1) int count |
@Max | 最大值 | @Max(100) int size |
@Positive | 必须是正数 | @Positive int timeout |
@Email | 邮箱格式 | @Email String email |
@Pattern | 正则匹配 | @Pattern(regexp="\\d+") |
技巧3:配置提示(Hints)
场景: 希望IDE提示可选值
手动编写元数据:
创建src/main/resources/META-INF/additional-spring-configuration-metadata.json:
{
"hints": [
{
"name": "hello.prefix",
"values": [
{
"value": "Hello",
"description": "英文问候"
},
{
"value": "你好",
"description": "中文问候"
},
{
"value": "こんにちは",
"description": "日文问候"
},
{
"value": "안녕하세요",
"description": "韩文问候"
}
]
},
{
"name": "hello.message.style",
"values": [
{
"value": "formal",
"description": "正式风格"
},
{
"value": "casual",
"description": "随意风格"
},
{
"value": "friendly",
"description": "友好风格"
}
]
}
]
}
效果:
hello:
prefix: # IDE会下拉提示:
# - Hello (英文问候)
# - 你好 (中文问候)
# - こんにちは (日文问候)
# - 안녕하세요 (韩文问候)
注意: 这个文件不会自动生成,需要手动创建!
技巧4:弃用配置项
场景: 某个配置项不再推荐使用
实现:
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
/**
* 前缀
*/
private String prefix = "Hello";
/**
* 老的前缀配置(已弃用)
* @deprecated 使用 prefix 替代
*/
@Deprecated
private String oldPrefix;
// getter/setter...
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
@Deprecated
public String getOldPrefix() {
return oldPrefix;
}
@Deprecated
public void setOldPrefix(String oldPrefix) {
this.oldPrefix = oldPrefix;
}
}
元数据中会自动标记:
{
"name": "hello.old-prefix",
"type": "java.lang.String",
"deprecated": true,
"deprecation": {
"reason": "使用 prefix 替代"
}
}
效果: IDE会显示删除线提示
📊 官方Starter元数据分析
案例:Spring Redis配置
查看源码: spring-boot-autoconfigure-xxx.jar
位置: META-INF/spring-configuration-metadata.json
节选内容:
{
"groups": [
{
"name": "spring.redis",
"type": "org.springframework.boot.autoconfigure.data.redis.RedisProperties"
}
],
"properties": [
{
"name": "spring.redis.client-type",
"type": "org.springframework.boot.autoconfigure.data.redis.RedisProperties$ClientType",
"description": "客户端类型,lettuce或jedis",
"defaultValue": "lettuce"
},
{
"name": "spring.redis.cluster.max-redirects",
"type": "java.lang.Integer",
"description": "集群模式下最大重定向次数"
},
{
"name": "spring.redis.host",
"type": "java.lang.String",
"description": "Redis服务器地址",
"defaultValue": "localhost"
},
{
"name": "spring.redis.port",
"type": "java.lang.Integer",
"description": "Redis服务器端口",
"defaultValue": 6379
},
{
"name": "spring.redis.timeout",
"type": "java.time.Duration",
"description": "连接超时时间"
}
]
}
学习点:
- 分组管理: 用
groups组织配置 - 类型多样: 支持String、Integer、Duration等
- 描述清晰: 每个配置都有说明
- 默认值明确: 让用户知道不配置会怎样
🎯 完整实战:改造我们的Starter
最终Properties代码
package com.example.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
/**
* Hello Starter配置属性
* <p>用于配置Hello功能的各项参数</p>
*
* @author YourName
* @since 1.0.0
*/
@ConfigurationProperties(prefix = "hello")
@Validated
public class HelloProperties {
/**
* 是否启用Hello功能
* <p>默认值:true</p>
* <p>设置为false可以禁用Hello功能</p>
*/
private boolean enabled = true;
/**
* 问候语前缀
* <p>默认值:Hello</p>
* <p>示例:你好、Hi、Welcome、Greetings</p>
* <p>不能为空</p>
*/
@NotBlank(message = "问候语前缀不能为空")
private String prefix = "Hello";
/**
* 问候语后缀
* <p>默认值:!</p>
* <p>示例:!、~、...、。</p>
* <p>不能为空</p>
*/
@NotBlank(message = "问候语后缀不能为空")
private String suffix = "!";
/**
* 最大消息长度
* <p>默认值:100</p>
* <p>范围:1-1000</p>
* <p>超过此长度的消息会被截断</p>
*/
@Min(value = 1, message = "最大长度不能小于1")
@Max(value = 1000, message = "最大长度不能大于1000")
private int maxLength = 100;
/**
* 超时时间(毫秒)
* <p>默认值:3000(3秒)</p>
* <p>范围:100-60000</p>
*/
@Min(value = 100, message = "超时时间不能小于100毫秒")
@Max(value = 60000, message = "超时时间不能大于60秒")
private int timeout = 3000;
// Getter 和 Setter
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
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;
}
public int getMaxLength() {
return maxLength;
}
public void setMaxLength(int maxLength) {
this.maxLength = maxLength;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
}
打包测试
# 1. 清理编译
mvn clean compile
# 2. 检查生成的元数据
cat target/classes/META-INF/spring-configuration-metadata.json
# 3. 安装到本地仓库
mvn install
测试效果
配置文件:
hello:
# 所有配置项都有智能提示和说明
enabled: true
prefix: 你好 # IDE提示:问候语前缀 默认值:Hello
suffix: ,欢迎! # IDE提示:问候语后缀 默认值:!
max-length: 200 # IDE提示:最大消息长度 范围:1-1000
timeout: 5000 # IDE提示:超时时间(毫秒) 范围:100-60000
错误配置会被IDE标红:
hello:
prefix: "" # ❌ 红色波浪线:问候语前缀不能为空
max-length: 9999 # ❌ 红色波浪线:最大长度不能大于1000
timeout: 99 # ❌ 红色波浪线:超时时间不能小于100毫秒
完美! 🎉
💡 知识点总结
本篇你学到了什么?
✅ 配置元数据的概念
- 让IDE提供智能提示的"菜单"
- 位置:
META-INF/spring-configuration-metadata.json
✅ 两种生成方式
- 手写JSON(不推荐)
- 自动生成(推荐)
✅ 自动生成三步曲
- 添加
spring-boot-configuration-processor依赖 - 在Properties类中写好JavaDoc注释
- 编译打包自动生成
✅ 进阶技巧
- 嵌套配置(内部类)
- 配置校验(@Validated)
- 配置提示(hints)
- 弃用标记(@Deprecated)
✅ 最佳实践
- 每个字段都写清楚描述
- 标明默认值和示例
- 添加校验注解
- 配置分组管理
🤔 思考题
问题1: 如果配置项非常多(比如50+个),如何优雅地组织?
提示: 嵌套配置 + 分组
问题2: 如果希望某个配置项的值从环境变量或系统属性读取,怎么做?
提示: SpringBoot的配置优先级
答案: 下一篇源码分析时会涉及!
📢 下期预告(系列最终篇)
《手写SpringBoot Starter(五):深度解析MyBatis Starter,学习大厂设计思想!》
下一篇我们将:
- 深入MyBatis Starter源码
- 分析Starter和Autoconfigure分离设计
- 学习SpringBoot自动装配原理
- 对比不同Starter的设计模式
- 总结企业级Starter设计最佳实践
系列完结篇,干货满满! 🚀
💬 互动时间
你觉得配置元数据重要吗?
你见过哪些Starter的配置体验特别好?
还想了解配置的哪些技巧?
欢迎评论区交流!💭
觉得有帮助?三连支持: 👍 点赞 | ⭐ 收藏 | 🔄 转发
看完这篇,你的Starter已经达到生产级别了! 💎
下一篇见! 👋