手写SpringBoot Starter(四):配置元数据让你的Starter拥有智能提示!

系列文章第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(不推荐)

步骤:

  1. 创建文件:src/main/resources/META-INF/spring-configuration-metadata.json

  2. 手写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;
    }
}

关键点:

  1. 类级别JavaDoc: 描述这个配置类的作用
  2. 字段级别JavaDoc: 描述每个配置项
    • 第一行:简短描述
    • <p>默认值:xxx</p>:说明默认值
    • <p>示例:xxx</p>:提供使用示例
  3. 必须有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>

如果之前引用过,需要:

  1. Maven → Reload Project
  2. 或者 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": "连接超时时间"
    }
  ]
}

学习点:

  1. 分组管理:groups组织配置
  2. 类型多样: 支持String、Integer、Duration等
  3. 描述清晰: 每个配置都有说明
  4. 默认值明确: 让用户知道不配置会怎样

🎯 完整实战:改造我们的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

两种生成方式

  1. 手写JSON(不推荐)
  2. 自动生成(推荐)

自动生成三步曲

  1. 添加spring-boot-configuration-processor依赖
  2. 在Properties类中写好JavaDoc注释
  3. 编译打包自动生成

进阶技巧

  • 嵌套配置(内部类)
  • 配置校验(@Validated)
  • 配置提示(hints)
  • 弃用标记(@Deprecated)

最佳实践

  • 每个字段都写清楚描述
  • 标明默认值和示例
  • 添加校验注解
  • 配置分组管理

🤔 思考题

问题1: 如果配置项非常多(比如50+个),如何优雅地组织?

提示: 嵌套配置 + 分组

问题2: 如果希望某个配置项的值从环境变量或系统属性读取,怎么做?

提示: SpringBoot的配置优先级

答案: 下一篇源码分析时会涉及!


📢 下期预告(系列最终篇)

《手写SpringBoot Starter(五):深度解析MyBatis Starter,学习大厂设计思想!》

下一篇我们将:

  • 深入MyBatis Starter源码
  • 分析Starter和Autoconfigure分离设计
  • 学习SpringBoot自动装配原理
  • 对比不同Starter的设计模式
  • 总结企业级Starter设计最佳实践

系列完结篇,干货满满! 🚀


💬 互动时间

你觉得配置元数据重要吗?
你见过哪些Starter的配置体验特别好?
还想了解配置的哪些技巧?

欢迎评论区交流!💭


觉得有帮助?三连支持: 👍 点赞 | ⭐ 收藏 | 🔄 转发

看完这篇,你的Starter已经达到生产级别了! 💎


下一篇见! 👋