⚙️ Vostok.Config
Config 配置管理模块
统一配置访问,自动扫描 .properties / .yml / .yaml 文件,支持多优先级覆盖、${key} 插值、文件热监听、可插拔验证器与解析器、类型安全 POJO 绑定,以及配置来源追踪。所有读操作无锁(基于 volatile 快照),线程安全。
初始化(可选)
零配置启动
// 无需显式初始化——首次读取配置时自动以默认选项懒加载。
String host = Vostok.Config.get("db.host");
显式初始化
// 以内置默认选项初始化(幂等,已初始化则忽略)
Vostok.Config.init();
// 自定义选项
Vostok.Config.init(new VKConfigOptions()
.watchEnabled(true) // 开启文件热监听
.watchDebounceMs(500) // 防抖延迟(ms,最小 50)
.loadEnv(true) // 加载环境变量(默认已开启)
.loadSystemProperties(true) // 加载 JVM 系统属性(默认已开启)
.scanClasspath(true) // 扫描 classpath(默认已开启)
.scanUserDir(true) // 扫描 user.dir(默认已开启)
.addScanDir(Path.of("/etc/myapp")) // 额外扫描目录
);
幂等初始化
init() 已初始化时静默忽略。如需强制替换配置,使用 reinit(VKConfigOptions)。
运行时修改选项(不替换)使用 configure(Consumer<VKConfigOptions>),会触发一次重载。
读取配置
基础读取
// 读取字符串(未找到返回 null)
String host = Vostok.Config.get("db.host");
// 必须存在,未找到抛 VKConfigException(KEY_NOT_FOUND)
String url = Vostok.Config.required("db.url");
// 读取字符串,含默认值
String host = Vostok.Config.getString("db.host", "localhost");
// 读取各类型(含默认值)
int port = Vostok.Config.getInt("db.port", 5432);
long timeout = Vostok.Config.getLong("conn.timeoutMs", 3000L);
double ratio = Vostok.Config.getDouble("pool.factor", 0.75);
boolean enabled = Vostok.Config.getBool("feature.enabled", false);
// 读取列表(逗号分隔,或 key[0]/key[1] 数组格式)
List<String> hosts = Vostok.Config.getList("db.hosts");
// 检查 key 是否存在
boolean exists = Vostok.Config.has("db.host");
// 获取所有已加载的 key
Set<String> allKeys = Vostok.Config.keys();
getBool() 识别规则:
| 值(不区分大小写) | 结果 |
|---|---|
true / 1 / yes / on | true |
false / 0 / no / off | false |
| 其他值 / 未找到 | 返回默认值 |
列表格式
getList(key) 支持两种配置格式:
# 格式 1:逗号分隔
db.hosts=host1,host2,host3
# 格式 2:下标数组
db.hosts[0]=host1
db.hosts[1]=host2
db.hosts[2]=host3
插值语法
配置值中可用 ${key} 引用其他配置项,支持默认值语法 ${key:defaultValue}。插值递归解析,循环引用时抛 VKConfigException。
# application.properties
base.url=https://api.example.com
retry.url=${base.url}/retry
# 引用环境变量,缺省 5000
timeout=${REQUEST_TIMEOUT:5000}
# 多级引用
app.name=MyApp
log.prefix=[${app.name}]
插值解析时机
插值在每次 get() / getString() 等读取时实时解析,不在加载时展开。因此 runtime override 修改了引用目标后,读取结果会立即反映新值。
配置优先级
从低到高,高优先级覆盖低优先级:
- classpath 配置文件(
scanClasspath=true) addScanDir()指定的额外扫描目录- user.dir(工作目录,
scanUserDir=true) - 手动追加的文件(
addFile()/addFiles()) - 环境变量(
loadEnv=true) - JVM 系统属性(
loadSystemProperties=true) - 运行时覆盖(
putOverride())
环境变量 key 规范化
环境变量 UPPER_CASE 会映射为 upper.case,两种形式同时可用。
系统属性中的 - 会规范化为 .。
运行时覆盖
// 写入运行时覆盖(最高优先级,立即生效)
Vostok.Config.putOverride("db.url", "jdbc:postgresql://prod-host/mydb");
// 传入 null 等同于 removeOverride
Vostok.Config.putOverride("db.url", null);
// 移除单个覆盖
Vostok.Config.removeOverride("db.url");
// 清空所有覆盖
Vostok.Config.clearOverrides();
手动追加配置文件
// 追加单个文件(立即加载,触发一次重载)
Vostok.Config.addFile("/etc/myapp/secrets.properties");
// 追加多个文件(全部校验通过后统一触发一次重载)
Vostok.Config.addFiles("/conf/a.yml", "/conf/b.yml");
// 移除所有手动追加的文件(触发重载)
Vostok.Config.clearManualFiles();
// 手动触发全量重载
Vostok.Config.reload();
文件变更监听
开启 watchEnabled(true) 后,Config 模块自动监听文件系统变更,防抖后触发重载并通知监听器。
addChangeListener — 全局监听器
VKConfigChangeListener listener = event -> {
// changedKeys():本次变更的所有 key(新增 / 修改 / 删除)
if (event.changedKeys().contains("db.url")) {
String oldUrl = event.oldValue("db.url"); // 变更前的值(新增时为 null)
String newUrl = event.newValue("db.url"); // 变更后的值(删除时为 null)
reconnect(newUrl);
}
};
Vostok.Config.addChangeListener(listener);
Vostok.Config.removeChangeListener(listener); // 注销
onChange — 单 key 快捷监听
// 仅在指定 key 变更时触发,返回 listener 实例供后续注销
VKConfigChangeListener l = Vostok.Config.onChange("server.port", (oldVal, newVal) -> {
System.out.println("port: " + oldVal + " → " + newVal);
});
// 注销时复用返回的 listener 引用
Vostok.Config.removeChangeListener(l);
VKConfigChangeEvent 完整方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
changedKeys() | Set<String> | 本次变更的所有 key(新增 / 修改 / 删除) |
oldValue(key) | String | 变更前的值;key 是新增时返回 null |
newValue(key) | String | 变更后的值;key 被删除时返回 null |
oldSnapshot() | Map<String,String> | 变更前的完整配置快照(只读) |
newSnapshot() | Map<String,String> | 变更后的完整配置快照(只读) |
配置校验
通过 VKConfigValidators 使用内置校验器,或实现 VKConfigValidator 接口自定义。每次重载后校验器会重新执行,失败时抛 VKConfigException(VALIDATION_ERROR)。
内置校验器
// 必填:key 必须存在且不为空白
Vostok.Config.registerValidator(VKConfigValidators.required("db.url", "db.port"));
// 整数范围:[min, max] 闭区间
Vostok.Config.registerValidator(VKConfigValidators.intRange("pool.size", 1, 200));
// 正整数(> 0)
Vostok.Config.registerValidator(VKConfigValidators.positiveInt("thread.count"));
// 端口范围:[1, 65535]
Vostok.Config.registerValidator(VKConfigValidators.portRange("server.port"));
// 枚举值(不区分大小写)
Vostok.Config.registerValidator(VKConfigValidators.oneOf("log.level", "TRACE", "DEBUG", "INFO", "WARN", "ERROR"));
// 正则格式(全匹配)
Vostok.Config.registerValidator(VKConfigValidators.pattern("app.version", "\\d+\\.\\d+\\.\\d+"));
// URL 格式(http / https 及自定义 scheme)
Vostok.Config.registerValidator(VKConfigValidators.url("api.baseUrl"));
跨字段校验
// cross:带名称的自定义谓词,可访问完整配置视图
Vostok.Config.registerValidator(VKConfigValidators.cross(
"ssl-consistency",
view -> !view.getBool("ssl.enabled", false) || view.has("ssl.certPath"),
"ssl.certPath is required when ssl.enabled=true"
));
自定义校验器
Vostok.Config.registerValidator(view -> {
String url = view.get("db.url");
if (url == null || !url.startsWith("jdbc:")) {
throw new IllegalStateException("db.url must be a valid JDBC URL");
}
});
// 移除所有校验器
Vostok.Config.clearValidators();
VKConfigView(校验器收到的视图)方法:
| 方法 | 说明 |
|---|---|
get(key) | 读取字符串,未找到返回 null |
has(key) | 检查 key 是否存在 |
keys() | 所有 key 集合 |
asMap() | 完整配置快照(只读 Map) |
getInt(key, default) | 解析整数 |
getLong(key, default) | 解析 long |
getDouble(key, default) | 解析 double |
getBool(key, default) | 解析布尔 |
getList(key) | 解析列表(逗号分隔或下标格式) |
类型安全绑定
将配置项自动绑定到 POJO 字段,支持 String、基本类型及其包装类、List<String>。POJO 需提供无参构造器。
使用 @VKConfigPrefix 注解
// 在 POJO 类上标注前缀
@VKConfigPrefix("database")
public class DatabaseConfig {
private String host; // ← database.host
private int port; // ← database.port
private boolean sslEnabled; // ← database.sslEnabled 或 database.ssl-enabled
private List<String> replicas; // ← database.replicas(逗号分隔)
}
// 绑定(通过注解自动取前缀)
DatabaseConfig cfg = Vostok.Config.bind(DatabaseConfig.class);
显式指定前缀
// 不需要注解,直接传入前缀字符串
public class RedisConfig {
private String host;
private int port;
private int maxConnections; // ← redis.maxConnections 或 redis.max-connections
}
RedisConfig cfg = Vostok.Config.bind("redis", RedisConfig.class);
字段名映射规则
每个字段按以下顺序尝试匹配(找到即停止):
prefix.fieldName(camelCase 原样)prefix.field-name(camelCase → kebab-case 转换)
支持的字段类型
| 字段类型 | 说明 |
|---|---|
String | 直接赋值 |
int / Integer | 解析为整数 |
long / Long | 解析为 long |
double / Double | 解析为 double |
float / Float | 解析为 float |
boolean / Boolean | 同 getBool() 规则 |
List<String> | 同 getList(),逗号分隔或下标格式 |
自定义解析器
实现 VKConfigParser 接口可支持任意文件格式(如 TOML、JSON、HOCON 等)。解析器注册后优先级最高,同时触发一次重载。
Vostok.Config.registerParser(new VKConfigParser() {
@Override
public boolean supports(String fileName) {
return fileName.endsWith(".toml");
}
@Override
public Map<String, String> parse(String sourceId, InputStream inputStream) {
// 解析文件内容,返回扁平化的 key-value Map
// sourceId 为来源标识(文件路径或 jar 内路径),可用于错误提示
Map<String, String> result = new LinkedHashMap<>();
// ... 解析逻辑 ...
return result;
}
});
内置格式
.properties(Java Properties 格式)和 .yml / .yaml(YAML,支持嵌套对象展平、序列、行内注释、引号值)无需注册,开箱即用。
来源追踪
用于调试排查:某个 key 当前读取的值来自哪个文件(或环境变量、系统属性、运行时覆盖)。
// 查询单个 key 的来源
String source = Vostok.Config.sourceOf("db.url");
// 可能的返回值示例:
// "/home/app/application.properties" — 文件路径
// "env" — 环境变量
// "system-property" — JVM 系统属性
// "runtime-override" — putOverride() 写入
// null — key 不存在
// 获取全部 key 的来源映射(只读快照)
Map<String, String> allSources = Vostok.Config.sources();
文件监听状态
// 是否已初始化(或已懒加载)
boolean ready = Vostok.Config.started();
// 上次文件监听出错时的错误信息(无错误时返回 null)
// 文件监听出错时保留旧配置快照,不影响读取
String watchErr = Vostok.Config.lastWatchError();
生命周期控制
// 以内置默认选项初始化(幂等)
Vostok.Config.init();
// 强制重新初始化(忽略已初始化状态)
Vostok.Config.reinit(new VKConfigOptions().watchEnabled(true));
// 修改当前选项(触发一次重载,不重置 listeners / validators / parsers)
Vostok.Config.configure(opts -> opts.watchDebounceMs(200));
// 手动触发全量重载
Vostok.Config.reload();
// 关闭(停止文件监听,清空所有状态)
Vostok.Config.close();
配置参数(VKConfigOptions)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| 扫描源 | |||
| scanClasspath | boolean | true | 扫描 classpath 下的配置文件(JAR 中最多扫描一层) |
| scanUserDir | boolean | true | 扫描 user.dir(工作目录)下的配置文件(递归) |
| addScanDir(Path) | — | — | 添加额外扫描目录(递归扫描),可多次调用;通过 getExtraScanDirs() 获取列表 |
| 外部来源 | |||
| loadEnv | boolean | true | 加载环境变量(UPPER_CASE → upper.case,两种形式均可用) |
| loadSystemProperties | boolean | true | 加载 JVM 系统属性(- 规范化为 .) |
| envProvider | Supplier<Map> | System::getenv | 自定义环境变量来源(测试时常用于注入 mock 环境) |
| systemPropertiesProvider | Supplier<Properties> | System::getProperties | 自定义系统属性来源 |
| 热监听 | |||
| watchEnabled | boolean | false | 开启文件系统监听,文件变更后自动重载配置 |
| watchDebounceMs | long | 300 | 文件变更防抖延迟(ms),最小 50ms |
| 冲突控制 | |||
| strictNamespaceConflict | boolean | false | 开启后,同一命名空间出现在多个来源时抛异常(严格模式) |
异常说明
| 错误码 | 触发场景 |
|---|---|
CK-404 KEY_NOT_FOUND | required(key) 读取不存在的 key |
CK-420 VALIDATION_ERROR | 校验器执行失败(含内置与自定义) |
CK-400 INVALID_ARGUMENT | 传入参数非法(如空 key、空路径) |
CK-402 CONFIG_ERROR | 插值循环引用等配置逻辑错误 |
CK-500 IO_ERROR | 文件读取失败 |
CK-510 PARSE_ERROR | 文件格式解析失败 |
API 速查
生命周期
| 方法 | 返回值 | 说明 |
|---|---|---|
init() | void | 以默认选项初始化(幂等) |
init(VKConfigOptions) | void | 以自定义选项初始化(幂等) |
reinit(VKConfigOptions) | void | 强制重新初始化 |
configure(Consumer<VKConfigOptions>) | void | 修改当前选项并触发重载 |
reload() | void | 手动触发全量重载 |
close() | void | 关闭并重置所有状态 |
started() | boolean | 是否已初始化(含懒加载) |
lastWatchError() | String | 上次文件监听错误信息,null 表示无错误 |
读取
| 方法 | 返回值 | 说明 |
|---|---|---|
get(key) | String | 读取字符串,未找到返回 null |
required(key) | String | 读取字符串,未找到抛 VKConfigException |
has(key) | boolean | 检查 key 是否存在 |
keys() | Set<String> | 所有已加载的 key |
getString(key, default) | String | 读取字符串,含默认值 |
getInt(key, default) | int | 解析整数,含默认值 |
getLong(key, default) | long | 解析 long,含默认值 |
getDouble(key, default) | double | 解析 double,含默认值 |
getBool(key, default) | boolean | 解析布尔,含默认值 |
getList(key) | List<String> | 解析列表(逗号分隔或下标格式) |
运行时覆盖与文件管理
| 方法 | 说明 |
|---|---|
putOverride(key, value) | 写入运行时覆盖(最高优先级),null value 等同 removeOverride |
removeOverride(key) | 移除指定运行时覆盖 |
clearOverrides() | 清空所有运行时覆盖 |
addFile(path) | 手动追加单个配置文件并重载 |
addFiles(paths...) | 手动追加多个文件,统一触发一次重载 |
clearManualFiles() | 移除所有手动追加的文件并重载 |
监听器 / 校验器 / 解析器
| 方法 | 说明 |
|---|---|
addChangeListener(VKConfigChangeListener) | 注册全局配置变更监听器 |
removeChangeListener(VKConfigChangeListener) | 注销监听器 |
onChange(key, BiConsumer<String,String>) | 单 key 变更快捷监听,返回 listener 实例 |
registerValidator(VKConfigValidator) | 注册校验器(每次重载后执行) |
clearValidators() | 移除所有校验器 |
registerParser(VKConfigParser) | 注册自定义文件格式解析器(最高优先级) |
绑定与来源追踪
| 方法 | 返回值 | 说明 |
|---|---|---|
bind(prefix, Class<T>) | T | 将指定前缀的配置绑定到 POJO 实例 |
bind(Class<T>) | T | 通过 @VKConfigPrefix 注解取前缀后绑定 |
sourceOf(key) | String | 返回 key 的来源标识(文件路径 / "env" / "system-property" / "runtime-override" / null) |
sources() | Map<String,String> | 全部 key 的来源映射(只读快照) |