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;只有调用 ConfigManager、Config、RuntimeConfigSource 或模块自己的 ConfigResolver 的运行期读取路径,才能获得数据库覆盖值。
开发期可以用下面的命令复查运行期配置边界:
./.ai/scripts/check config-runtime该检查会扫描生产源码,阻止新增 Environment#getProperty("spring.open.*") 或 @Value("${spring.open.*}") 这类绕过配置中心的运行期读取;@ConfigurationProperties、条件装配、端口、数据源、Redis 等启动期基础设施配置仍按 Spring Boot 原生机制处理。
快速开始
spring:
open:
config:
enabled: true
provider: database
database:
namespace: default
fallback-to-environment: true业务代码:
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");读取顺序:
数据库配置项 → 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 ConversionService:String / Integer / Long / Boolean / Duration / Enum 等。
配置项
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| 配置 | 默认值 | 说明 |
|---|---|---|
enabled | true | 启用开关 |
provider | environment | 应用聚合默认覆盖为 database |
key-prefix | config:value | 缓存 key 前缀 |
database.namespace | default | 数据库命名空间 |
database.fallback-to-environment | true | 数据库无值时回退本地 |
cache.ttl | 5m | 缓存时间 |
cache.remote-enabled | false | 启用远程缓存 |
refresh.provider | application | 多实例广播 provider |
change-log.enabled | true | 配置变更日志入口 |
spel.enabled | true | 配置文件 SpEL 求值开关 |
spel.include-system-sources | false | 是否对非配置文件来源也求值 |
spel.fail-on-error | true | 表达式无效时启动报错 |
环境变量遵循 Spring Boot 宽松绑定:
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 变量都默认不可用。
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,未配置时可通过应用名和本机设备标识派生本地兜底值:
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 显式放宽。
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));
}
}# META-INF/spring.factories
com.springopen.starter.config.spel.ConfigSpelContextCustomizer=\
com.example.MyConfigSpelCustomizerapp:
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() 口径一致:
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 与 JDBCserverTimezone;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(...) 换算链:
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 Environment | starter 裸用、测试、无数据库后台 |
database | 先读 config_entry,回退 Environment | 默认应用,提供后台可维护配置 |
spring-open-starter-config 默认 provider 为 environment;spring-open-application 默认覆盖为 database。
刷新 Provider:
| Provider | 说明 |
|---|---|
application | Spring 应用事件,单实例足够 |
redis | 通过 Redis Pub/Sub 通知其他实例 |
扩展:实现 ConfigChangePublisher SPI 接入 MQ 或数据库轮询。
源码机制骨架
Config starter 的核心链路是:
业务代码
→ Config / ConfigManager
→ CachedConfigProvider
→ ConfigProvider
→ Environment 或 Database| 层级 | 类 / 接口 | 职责 |
|---|---|---|
| Facade | Config | 给业务代码提供短名静态 API;不保存业务状态 |
| Manager | ConfigManager / DefaultConfigManager | 校验 key、调用 provider、完成字符串到目标类型的转换 |
| Provider SPI | ConfigProvider | 从一个来源读取原始字符串值;不做缓存和类型转换 |
| Environment provider | EnvironmentConfigProvider | 读取 Spring Boot 已解析的配置源 |
| Database provider | DatabaseConfigProvider | 读取数据库配置,未命中时按配置回退 Environment |
| Repository bridge | DatabaseConfigRepository | 由 Config 核心模块或应用层提供具体数据库读取实现,Config starter 不直接依赖业务实体 |
| Cache decorator | CachedConfigProvider | 按本地缓存、远程缓存、真实 provider 的顺序读取和回填 |
| Refresh | ConfigRefreshManager / ConfigCacheRefresher | 后台配置变更后清理缓存并广播失效事件 |
| Broadcast SPI | ConfigChangePublisher | 把失效事件发布到应用事件、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 或具体实现模块包名。- 刷新事件只携带
namespace和key,不要把配置值、token、私钥或密钥 hint 放进应用事件和 Redis 消息。 - 端口、数据源、Redis 地址、
@ConditionalOnProperty装配开关、调度任务注册开关等启动期配置仍走.env/application.yml;Config starter 不承诺运行时修改这类早期启动配置后立即重建基础设施 Bean。 @ConfigurationProperties只代表启动时已绑定的本地 / 环境配置。希望后台配置中心即时生效的模块,必须在业务动作路径上显式使用ConfigManager、ConfigResolver或RuntimeConfigSource,并在模块文档中写清楚“配置中心生效”与“仅启动期生效”的边界。
数据库配置项
底层表: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 之后。
业务代码无需关心顺序,直接构造器注入:
public class SampleConfigService {
private final ConfigManager configManager;
public SampleConfigService(ConfigManager configManager) {
this.configManager = configManager;
}
}自定义 starter 需要读后台配置时显式声明:
@AutoConfiguration(after = ConfigAutoConfiguration.class)
public class SampleAutoConfiguration { }项目内禁止使用 afterName = "com.springopen.xxx" 字符串写法;优先 after = XxxAutoConfiguration.class。
站点公开信息
前端读取统一接口:
GET /api/v1/site默认值来自 spring.open.site.*,管理员可在 config_entry 覆盖:
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: falseurl显式优先;为空时按当前请求推断- 反向代理识别需配置 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
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)配置分类:
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):
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 / 操作人。
缓存与刷新
local cache → remote cache → provider缓存 key 包含 namespace:config:value:{namespace}:{key}
写入流程:
ConfigService
→ ConfigRefreshManager
→ ConfigCacheRefresher
→ ConfigChangePublisher单实例 application provider 用 Spring 应用事件;多实例切 redis:
spring:
open:
config:
refresh:
enabled: true
provider: redis
redis-channel: config:changed敏感值自动脱敏
后台展示和变更日志中命中以下语义自动转 ******:
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 - 读配置走
ConfigFacade;写配置走后台 API 或ConfigService - 不绕过 Service 直接更新
config_entry,否则不触发缓存刷新和变更日志 - 业务模块新增可后台维护配置时同步补
configs/metadata - 单元测试可通过
MockEnvironment或 mockConfigManager注入
验证命令
./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相关
- 启动期配置(
.env/application.yml):configuration.md - 缓存底座:cache.md
- 模块架构:architecture.md
- 部署与反向代理:deployment.md
- Provider 扩展模型:provider.md