跳到内容

Config Starter

受众:开发者 摘要:运行时配置中心 Facade。Environment / Database provider、缓存链路、多实例广播、通过 Config Facade 覆盖本地配置。

何时使用

场景建议
启动期生效的配置(端口、数据库连接).env / application.yml,看 configuration.md
运行时管理员可调整的配置(开关、第三方密钥、签名)Config.get(...) + database provider
站点公开信息(名称、Logo、备案、联系方式)GET /api/v1/site
排查配置最终值GET /api/v1/config/configs/value
多实例下后台改完所有节点立即生效启用 refresh.provider=redis
业务自己的配置前缀(shop.* / tenant.*也可走 Config facade,无需 spring.open.*

业务代码统一用 Config Facade 或注入 ConfigManager直接读 Environment,避免绕过数据库覆盖。配置中心不会自动回灌所有 @ConfigurationProperties Bean;只有调用 ConfigManagerConfigRuntimeConfigSource 或模块自己的 ConfigResolver 的运行期读取路径,才能获得数据库覆盖值。

开发期可以用下面的命令复查运行期配置边界:

bash
./.ai/scripts/check config-runtime

该检查会扫描生产源码,阻止新增 Environment#getProperty("spring.open.*")@Value("${spring.open.*}") 这类绕过配置中心的运行期读取;@ConfigurationProperties、条件装配、端口、数据源、Redis 等启动期基础设施配置仍按 Spring Boot 原生机制处理。

快速开始

yaml
spring:
  open:
    config:
      enabled: true
      provider: database
      database:
        namespace: default
        fallback-to-environment: true

业务代码:

java
String siteName = Config.get("spring.open.site.name", "Sample App");
boolean enabled = Config.get("feature.payment.enabled", Boolean.class, false);
String secret = Config.getRequired("spring.open.payment.providers.alipay.private-key");

读取顺序:

text
数据库配置项 → Spring Environment → 默认值

Facade API

方法说明
Config.get(key)获取字符串,不存在返回 null
Config.get(key, defaultValue)不存在返回默认值
Config.get(key, type)类型转换,不存在返回 null
Config.get(key, type, defaultValue)类型转换 + 默认值
Config.getRequired(key)不存在抛异常
Config.getRequired(key, type)类型转换,不存在抛异常
Config.contains(key)是否存在

类型转换复用 Spring ConversionServiceString / Integer / Long / Boolean / Duration / Enum 等。

配置项

yaml
spring:
  open:
    config:
      enabled: true
      provider: database                  # environment / database
      key-prefix: config:value
      database:
        namespace: default
        fallback-to-environment: true
      cache:
        enabled: true
        local-enabled: true
        local-store: memory
        remote-enabled: false
        remote-store: redis
        ttl: 5m
      refresh:
        enabled: true
        provider: application             # application / redis
        redis-channel: config:changed
      change-log:
        enabled: true
      spel:
        enabled: true                     # 配置文件支持 SpEL(#{...})
        include-system-sources: false     # 默认只对配置文件求值,不碰环境变量/命令行
        fail-on-error: true
配置默认值说明
enabledtrue启用开关
providerenvironment应用聚合默认覆盖为 database
key-prefixconfig:value缓存 key 前缀
database.namespacedefault数据库命名空间
database.fallback-to-environmenttrue数据库无值时回退本地
cache.ttl5m缓存时间
cache.remote-enabledfalse启用远程缓存
refresh.providerapplication多实例广播 provider
change-log.enabledtrue配置变更日志入口
spel.enabledtrue配置文件 SpEL 求值开关
spel.include-system-sourcesfalse是否对非配置文件来源也求值
spel.fail-on-errortrue表达式无效时启动报错

环境变量遵循 Spring Boot 宽松绑定:

dotenv
SPRING_OPEN_CONFIG_PROVIDER=database
SPRING_OPEN_CONFIG_DATABASE_NAMESPACE=default
SPRING_OPEN_CONFIG_CACHE_REMOTE_ENABLED=true
SPRING_OPEN_CONFIG_REFRESH_PROVIDER=redis

配置文件 SpEL 求值

引入 spring-open-starter-config 后,全局配置文件默认支持受控 SpEL(#{...})写法。求值由 ConfigSpelEnvironmentPostProcessor 在应用上下文创建前完成,默认只开放字面量、运算符、配置占位符组合和已注册函数;T(...) 类型引用、构造器、任意对象方法调用、Spring bean 引用和内置 #env 变量都默认不可用。

yaml
app:
  base-size: 4
  pool-size: "#{${app.base-size} * 2}"          # #{} 内可嵌 ${} 引用其它配置项
  upper-name: "#{#upper('spring-open')}"        # 使用内置纯函数,不调用任意对象方法
  • 求值时机在 bean 之前:可用普通配置项、${} 占位符、字面量、运算符和已注册函数;无法引用 Spring bean。需要 bean 的表达式请用 @Value("#{@beanName.method()}")
  • 默认安全面受控:默认阻断 T(...) 类型引用、构造器、任意对象方法调用和 #env。后台 / 数据库可编辑配置不得依赖这些能力;需要访问环境值时优先用 ${ENV_NAME:default} 占位符或显式注册受控函数。
  • 默认只对配置文件求值:只处理 application.yml / properties 等配置文件来源;环境变量、命令行、系统属性默认不求值,求值结果以紧贴原始来源之上的覆盖来源注入,保持各来源原有优先级。
  • 支持配置间组合${} 可以引用普通配置值,也可以引用同一轮已经由 SpEL 求值出的配置值;求值器会做有限多轮处理,仍无法解析时按 fail-on-error 策略处理。
  • #{}${} 的区别${} 是占位符(Environment 解析),#{} 是 SpEL(本特性 + @Value 求值);两者可组合。
  • 字面量 #{...}:配置文件里只要出现 #{ 就会按 SpEL 处理;如果需要保留原始文本,请关闭 spel.enabled,或改用不含 #{ 的占位写法。
  • 内置函数:默认注册 #slug#pascal#year()#lower#upper#trim#kebab#snake#envKey#dotKey#deviceId()#generateSecret(value)#generateSecret(value, algorithm) 等常用函数。#slug('Spring Open') 返回 spring-open,适合日志名、线程名前缀和 key 前缀;#pascal('spring-open') 返回 SpringOpen,适合站点名、OpenAPI 标题等展示名;#year() 返回当前年份,适合版权默认文案。#deviceId() 返回本机稳定节点标识(优先显式 spring.open.device-id / SPRING_OPEN_DEVICE_ID,其次首启在 storage/runtime/device-id 生成并复用的 32 字节随机标识——路径可用 spring.open.device-id-file / SPRING_OPEN_DEVICE_ID_FILE 覆盖,文件不可用时回退 machine-id、网卡硬件地址、hostname);#generateSecret(value) 默认对传入值派生 SHA-256 摘要,也可显式传入 SHA-384 / SHA-512,可组合应用名、分隔符、设备标识等配置值作为本地 / 单机兜底密钥。
  • 失败信息不打印配置原文:表达式无效时只报告配置 key、来源和异常类型,避免密码、token 等敏感配置进入日志。

应用共享密钥默认使用 APP_SECRET,未配置时可通过应用名和本机设备标识派生本地兜底值:

yaml
spring:
  application:
    secret: "${APP_SECRET:#{#generateSecret(#slug('${spring.application.name}') + ':' + #deviceId())}}"

派生兜底值的节点标识默认来自首启生成的随机文件,单机场景下高熵且跨重启稳定;容器部署把 storage/ 挂载为卷即可在容器重建后保持不变。启用数据库配置中心时,Config 模块首次启动还会自动在配置中心播种一条高熵随机的 spring.application.secret,JWT 签名、AI 与开放平台加密 key 等能力统一经 ApplicationSecretResolver 优先按该行收敛:多实例共享同一数据库即可零配置获得一致密钥。多实例不共享数据库配置中心、或需要跨环境迁移已加密数据时,仍应显式配置同一个强随机 APP_SECRET 或模块专用密钥。

自定义求值上下文

实现 ConfigSpelContextCustomizer 即可向 SpEL 注册自定义变量、函数或受控解析器,让配置里的表达式调用项目能力。通过 META-INF/spring.factories 注册,须有无参构造器;回调在上下文创建前发生,无法引用 bean。默认上下文已经阻断危险解析能力,只有完全受信任的本地配置场景才应通过 SPI 显式放宽。

java
public class MyConfigSpelCustomizer implements ConfigSpelContextCustomizer {
    @Override
    public void customize(StandardEvaluationContext context, ConfigurableEnvironment environment) {
        context.setVariable("appName", environment.getProperty("spring.application.name"));
        context.registerFunction("base64",
                MyConfigSpelCustomizer.class.getMethod("base64", String.class));
    }
    public static String base64(String value) {
        return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8));
    }
}
properties
# META-INF/spring.factories
com.springopen.starter.config.spel.ConfigSpelContextCustomizer=\
com.example.MyConfigSpelCustomizer
yaml
app:
  greeting: "#{#appName + ' ready'}"
  token: "#{#base64('secret')}"

安全提示

即使默认上下文已收紧,配置 SpEL 仍是启动前代码化配置入口。include-system-sources=true 会把 SpEL 求值入口扩展到环境变量、命令行参数、系统属性和其它可枚举配置来源,只应在完全受信任的部署环境开启。无需该能力时设 spel.enabled=false 关闭;不要让后台 / 数据库可编辑配置注册任意类型访问、构造器或通用方法调用能力。

求值器基于 Spring Boot 4 的 org.springframework.boot.EnvironmentPostProcessor(经 META-INF/spring.factories 注册),非已弃用的 org.springframework.boot.env.EnvironmentPostProcessor

应用统一时区

多区域集群部署时各节点宿主时区可能不同,而业务时间(凭证过期、配额窗口、结算取价、账本时间)统一使用 LocalDateTime。配置应用统一时区后,Config starter 会在 ConfigData 加载完成后立即把 JVM 默认时区固定为该值,保证全集群 LocalDateTime.now() 口径一致:

yaml
spring:
  open:
    app:
      # IANA 时区 ID;未配置时保持宿主时区
      time-zone: Asia/Shanghai
  • 生效范围:JVM 默认时区(TimeZone.setDefault)与 user.timezone 系统属性;spring.jackson.time-zone 未显式配置时也回落到该值。
  • 非法时区 ID 直接启动失败,不做静默回退。
  • 部署切换:应用模板已把 SPRING_OPEN_APP_TIME_ZONE 贯通到 JVM 时区、Jackson 与 JDBC serverTimezone;Docker Compose 用 APP_TZ 同步应用容器与数据库容器的 TZ。跨区迁移只改一个环境变量整体生效;生产运行后切换时区需按生产部署文档「切换应用时区」流程做存量数据换算。
  • 极早期日志:EPP 在 ConfigData 后执行,之前的启动日志时间戳使用宿主时区。容器部署由 APP_TZ(容器 TZ)覆盖;裸 JVM 部署可加 -Duser.timezone=<zone> 启动参数补齐。
  • 决策背景见 ADR-073:统一应用时区 + LocalDateTime,不做全面 Instant/UTC 存储化改造。

统一时间 API

spring-open-starter-common 提供纯 Java 门面 com.springopen.starter.common.time.Time,收敛时间读取、跨时区换算与测试时钟,业务代码不要手写 atZone(...) 换算链:

java
LocalDateTime now = Time.now();                    // 应用时区业务时间(无覆盖时等价 LocalDateTime.now())
ZoneId zone = Time.zone();                         // 当前应用统一时区

// 按用户时区展示 / 接收用户本地时间输入
LocalDateTime display = Time.toZone(order.getCreatedAt(), userZone);
LocalDateTime business = Time.fromZone(userInput, userZone);

// 与跨系统时间戳互转
Instant instant = Time.toInstant(order.getCreatedAt());
LocalDateTime fromEpoch = Time.fromInstant(Instant.ofEpochMilli(millis));

// 切换应用时区时的存量墙钟重解释(运维换算原语)
LocalDateTime shifted = Time.shift(stored, ZoneId.of("Asia/Shanghai"), ZoneId.of("UTC"));

// 测试注入固定时钟
try (Time.Scope ignored = Time.useFixed(LocalDateTime.of(2026, 7, 10, 12, 0))) {
    // Time.now() / Time.today() / Time.instant() 全部固定
}

Provider 扩展

Provider说明适用场景
environment直接读 Spring Environmentstarter 裸用、测试、无数据库后台
database先读 config_entry,回退 Environment默认应用,提供后台可维护配置

spring-open-starter-config 默认 provider 为 environmentspring-open-application 默认覆盖为 database

刷新 Provider

Provider说明
applicationSpring 应用事件,单实例足够
redis通过 Redis Pub/Sub 通知其他实例

扩展:实现 ConfigChangePublisher SPI 接入 MQ 或数据库轮询。

源码机制骨架

Config starter 的核心链路是:

text
业务代码
  → Config / ConfigManager
  → CachedConfigProvider
  → ConfigProvider
  → Environment 或 Database
层级类 / 接口职责
FacadeConfig给业务代码提供短名静态 API;不保存业务状态
ManagerConfigManager / DefaultConfigManager校验 key、调用 provider、完成字符串到目标类型的转换
Provider SPIConfigProvider从一个来源读取原始字符串值;不做缓存和类型转换
Environment providerEnvironmentConfigProvider读取 Spring Boot 已解析的配置源
Database providerDatabaseConfigProvider读取数据库配置,未命中时按配置回退 Environment
Repository bridgeDatabaseConfigRepository由 Config 核心模块或应用层提供具体数据库读取实现,Config starter 不直接依赖业务实体
Cache decoratorCachedConfigProvider按本地缓存、远程缓存、真实 provider 的顺序读取和回填
RefreshConfigRefreshManager / ConfigCacheRefresher后台配置变更后清理缓存并广播失效事件
Broadcast SPIConfigChangePublisher把失效事件发布到应用事件、Redis 或后续 MQ 通道

扩展新的配置来源时,只实现 ConfigProvider 并注册为 Bean,再把 spring.open.config.provider 指向新的 provider 名称。扩展多实例刷新方式时,只实现 ConfigChangePublisher,不要在 provider 里直接操作缓存。

变更事件只传 namespace 和 key,不传配置值。这样可以避免密钥类配置出现在 Redis 频道、 应用事件日志或调试输出里。

扩展边界

  • Config starter 只承载运行时读配置、类型转换、缓存装饰和刷新广播;写配置、权限、审计、脱敏展示由 Config Service 和后台 API 承接。
  • 新增配置来源只实现 ConfigProvider,返回原始字符串值;业务校验、密钥解析和状态变更不放在 provider。
  • DatabaseConfigRepository 是应用层桥接点,starter 不直接依赖 config_entry 实体、Mapper 或具体实现模块包名。
  • 刷新事件只携带 namespacekey,不要把配置值、token、私钥或密钥 hint 放进应用事件和 Redis 消息。
  • 端口、数据源、Redis 地址、@ConditionalOnProperty 装配开关、调度任务注册开关等启动期配置仍走 .env / application.yml;Config starter 不承诺运行时修改这类早期启动配置后立即重建基础设施 Bean。
  • @ConfigurationProperties 只代表启动时已绑定的本地 / 环境配置。希望后台配置中心即时生效的模块,必须在业务动作路径上显式使用 ConfigManagerConfigResolverRuntimeConfigSource,并在模块文档中写清楚“配置中心生效”与“仅启动期生效”的边界。

数据库配置项

底层表:config_entry

字段说明
namespace命名空间,默认 default
config_key配置 key
config_value配置值
description配置说明
enabled是否启用
created_by / updated_by操作人
optimistic_lock_version乐观锁
deleted_at逻辑删除

启用且未删除的配置优先级高于本地文件。fallback-to-environment=true 时数据库无值回退本地。

自动装配顺序

Config 排在 Cache 之后;Mail / SMS / Storage / Payment / Notification / Translate / Web3 等通过 @AutoConfiguration(after = ConfigAutoConfiguration.class) 排在 Config 之后。

业务代码无需关心顺序,直接构造器注入:

java
public class SampleConfigService {
    private final ConfigManager configManager;
    public SampleConfigService(ConfigManager configManager) {
        this.configManager = configManager;
    }
}

自定义 starter 需要读后台配置时显式声明:

java
@AutoConfiguration(after = ConfigAutoConfiguration.class)
public class SampleAutoConfiguration { }

项目内禁止使用 afterName = "com.springopen.xxx" 字符串写法;优先 after = XxxAutoConfiguration.class

站点公开信息

前端读取统一接口:

http
GET /api/v1/site

默认值来自 spring.open.site.*,管理员可在 config_entry 覆盖:

yaml
spring:
  open:
    site:
      name: My App
      title: My App
      description: "Spring Boot 多模块、Starter 化、AI Friendly Framework"
      keywords: "Spring Boot,Java,Starter,AI Friendly"
      url: ""
      auto-detect-url: true
      logo: /images/logo/logo.png
      favicon: /favicon.ico
      icp: ""
      police-record: ""
      contact:
        email: ""
        phone: ""
      copyright:
        owner: My App
        text: "© #{#year()} My App. All rights reserved."
      web3:
        display-mode: web3-env
      access:
        status: open
        message: ""
        resume-at: ""
        allow-admin: true
        allow-api-docs: false
  • url 显式优先;为空时按当前请求推断
  • 反向代理识别需配置 Spring Boot 可信转发头
  • 备案、联系方式、版权属于展示信息,放密钥
  • web3.display-mode 控制用户端 Web3 入口可见性:web3-env 仅 DApp / 注入钱包环境显示,always 始终显示,off 始终关闭;该配置只控制前端入口展示,不改变钱包签名能力校验
  • access.status 控制站点访问状态:open 正常开放,closed 关闭普通业务访问,maintenance 维护升级
  • access.message 是关闭 / 维护时展示给用户的说明文本;为空时 App / PC 不展示正文,只展示状态标题
  • access.resume-at 是预计恢复时间展示值,不触发自动恢复
  • access.allow-admin 默认 true,关闭 / 维护时仍允许后台、登录和配置接口进入,便于管理员恢复站点
  • access.allow-api-docs 默认 false,需要维护期开放 OpenAPI 文档时再显式开启
  • spring.open.site.enabled 只控制 Site 模块装配,不是站点开关;关闭或维护站点时不要禁用 Site 模块,否则前端无法读取状态说明
  • 后台字段说明读取 GET /api/v1/config/configs/metadata

配置分类

分类用于后台管理和检索,参与运行时优先级。

匹配规则:

  • namespace 完整相等
  • config_key 用「直属配置项」匹配,只匹配 match_code.<单段>
  • 前缀按 . 分段,spring.open 不匹配 spring.openx
  • 剩余仍含 . 时属于下一层分类

示例:分类 spring.open.sms

配置 key是否直属
spring.open.sms否(分类本身)
spring.open.sms.name
spring.open.sms.code.ids否(下一层)

分类树只维护 parent_id,不存冗余祖级路径。

后台 API

text
GET    /api/v1/config/configs                    # 分页(config:entry:view)
GET    /api/v1/config/configs/{id}               # 详情
POST   /api/v1/config/configs                    # 创建(config:entry:create)
PUT    /api/v1/config/configs/{id}               # 修改(config:entry:update)
PUT    /api/v1/config/configs/{id}/enabled       # 启停
DELETE /api/v1/config/configs/{id}               # 删除(config:entry:delete)
DELETE /api/v1/config/configs                    # 批量删除
GET    /api/v1/config/configs/value              # 排查最终生效值
GET    /api/v1/config/configs/metadata           # 字段元数据
POST   /api/v1/config/configs/refresh            # 手动刷新缓存和多实例广播(config:entry:update)

配置分类:

text
GET    /api/v1/config/categories
GET    /api/v1/config/categories/tree
POST   /api/v1/config/categories
PUT    /api/v1/config/categories/{id}
PUT    /api/v1/config/categories/{id}/enabled
DELETE /api/v1/config/categories/{id}
POST   /api/v1/config/categories/match    # 预览匹配

变更日志(config_change_log):

text
GET    /api/v1/config/change-logs
GET    /api/v1/config/change-logs/{id}
DELETE /api/v1/config/change-logs/{id}

变更日志记录:配置 ID / namespace / key / 操作类型(create / update / enable / disable / delete)/ 脱敏后旧值新值 / traceId / IP / User-Agent / 操作人。

缓存与刷新

text
local cache → remote cache → provider

缓存 key 包含 namespace:config:value:{namespace}:{key}

写入流程:

text
ConfigService
  → ConfigRefreshManager
  → ConfigCacheRefresher
  → ConfigChangePublisher

单实例 application provider 用 Spring 应用事件;多实例切 redis

yaml
spring:
  open:
    config:
      refresh:
        enabled: true
        provider: redis
        redis-channel: config:changed

敏感值自动脱敏

后台展示和变更日志中命中以下语义自动转 ******

text
password / passwd / secret / credential / private-key / api-key
access-key / client-secret / secret-id / secret-key
access-token / refresh-token / token / passphrase

建议:

  • 密钥、私钥、token、授权码放部署平台密钥、环境变量或后台配置
  • 把敏感值提交到 Git
  • 在日志中打印明文

开发约定

  • 框架级和第三方服务商用 spring.open.* key
  • 业务配置用业务前缀(shop.* / tenant.*
  • 多租户用 namespace 隔离,默认 default
  • 读配置走 Config Facade;写配置走后台 API 或 ConfigService
  • 绕过 Service 直接更新 config_entry,否则不触发缓存刷新和变更日志
  • 业务模块新增可后台维护配置时同步configs/metadata
  • 单元测试可通过 MockEnvironment 或 mock ConfigManager 注入

验证命令

bash
./mvnw -pl spring-open-starters/spring-open-starter-config -am test -DskipITs
./mvnw -pl spring-open-core/spring-open-core-config -am -Dtest='Config*Test' -Dsurefire.failIfNoSpecifiedTests=false test -DskipITs
./mvnw -pl spring-open-application -am test -DskipITs -DskipFrontend

相关

Released under the Apache License 2.0.