Repository navigation
Config
把类路径中的默认 YAML 提取到插件数据目录,并映射为 Kotlin 主构造器模型或 Java 8 POJO。支持默认值合并、注释保留、文件监听、原子重载、版本迁移和目录型配置注册表。
| 模块名 | 制品 | 自动带入 |
|---|---|---|
config |
me.kzheart.klib:klib-config |
core |
适合:
- 用类型安全对象替代散落的字符串路径读取;
- 首次启动时生成默认配置,版本升级时补入新增默认项;
- 修改 YAML 后自动刷新运行态或重建某个作用域;
- 配置格式演进时保留用户值与注释;
- 从一个目录加载多个同类型 YAML 定义。
klib { modules { config() } }依赖、直接引用制品和打包方式见 构建与打包。
1. 在资源目录放默认文件:
# src/main/resources/defaults/config.yml
debug: false
heartbeatTicks: 1200
database:
host: localhost
port: 33062. 创建配置类型。
- Kotlin 类型直接使用主构造器,支持
data class、val、默认参数与可空类型,见下方「Kotlin 配置模型」。 - Java POJO 字段可以不是
public,但类型必须有无参数构造器。 - Java
final字段只要 YAML 里出现同名键就会加载失败,Java 参与映射的字段不要声明为final。
public final class Settings {
public boolean debug;
public long heartbeatTicks = 1200L;
public Database database = new Database();
public static final class Database {
public String host = "localhost";
public int port = 3306;
}
}3. 标注文件路径:
@ConfigFile("config.yml")
public final class Settings { /* 同上 */ }4. 在插件的 setup() 中安装能力并加载文档:
@Override
protected void setup() {
ConfigModule.install(this);
ConfigDocument<Settings> config = configs().load(Settings.class);
logger().info("数据库地址:"
+ config.value().database.host
+ ":"
+ config.value().database.port);
}首次加载时,defaults/config.yml 会提取为插件数据目录中的 config.yml。
| 调用 | 说明 |
|---|---|
ConfigModule.install(this) |
简化默认安装:插件数据目录、插件类加载器、资源目录 defaults;未知键默认 WARN
|
ConfigModule.install(this, mapper) |
同一默认安装路径,使用显式未知键策略和自定义转换器 |
接收 Scope 的 ConfigModule.install 重载 |
需要其他目录、迁移提供器或映射器时使用;根作用域通过 context().scope() 取得 |
configs().load(Settings.class) |
从 @ConfigFile 取得文件路径 |
configs().load(Settings.class, "config.yml") |
没有 @ConfigFile 时使用 |
请求路径必须是数据目录内的非空相对路径,../ 等越界路径会被拒绝。
原生 Kotlin API 随 config 模块提供,无需额外模块;配置对象仍由同一文档与作用域管理。
kotlin-reflect 是构造器映射所需的运行时依赖;Klib Gradle 插件随模块依赖闭包打包并处理其元数据。反射重定位仅支持构建插件声明的固定官方版本,不要自行替换任意 reflect JAR;完整打包规则见构建与打包。
import me.kzheart.klib.config.ConfigModule
import me.kzheart.klib.config.annotation.ConfigFile
import me.kzheart.klib.config.annotation.Key
import me.kzheart.klib.config.annotation.Range
import me.kzheart.klib.config.load
@ConfigFile("config.yml")
data class Settings(
val debug: Boolean = false,
@field:Key("heartbeat-ticks") val heartbeatTicks: Long = 1200,
val database: Database = Database(),
val message: String? = null,
)
data class Database(
val host: String = "localhost",
@field:Range(min = 1.0, max = 65535.0) val port: Int = 3306,
)
// 在 KPlugin 子类中,先安装能力,再加载同一作用域持有的文档
override fun setup() {
ConfigModule.install(this)
val config = configs().load<Settings>()
val named = configs().load<Settings>("config.yml")
}示例的默认资源使用 heartbeat-ticks 键;@field:Key 的名称按原样匹配,不会把点号拆成路径。
| 主构造器参数 | YAML 缺失 | YAML 显式 null
|
|---|---|---|
| 非空、有默认参数 | 执行 Kotlin 默认参数 | 失败,报告字段路径 |
| 可空、有默认参数 | 执行 Kotlin 默认参数 | 传入 null,不使用默认参数 |
| 非空、无默认参数 | 失败,参数必填 | 失败,报告字段路径 |
| 可空、无默认参数 | 失败,参数仍必填 | 传入 null
|
- 使用主构造器实际调用与默认参数,不要求 Kotlin 无参构造器,也不反射修改主构造器的
val。 -
List<T>、Set<T>、Map<String, T>、数组与嵌套对象保留元素的可空约束;例如List<String>的 null 元素报告names[1],List<String?>接受它。 - 类体内的额外
var属性按字段映射并校验可空类型;额外val不可写。计算属性没有 backing field,不参与映射。未赋值的非空lateinit属性会加载失败。 -
@Key、@Range、@Comment使用@field:;@Validate放在公开无参、返回Unit的实例方法上。 - 需要普通、有主构造器的非 inner 类;不支持 object、vararg 构造参数、星投影或尚未确定实际类型的泛型参数。
- 构造器、类型或校验失败都进入原有原子重载流程,保留先前有效对象,监听器不会收到半成品。
直接映射已取得的节点或显式生成默认 YAML:
import me.kzheart.klib.config.configDefaults
import me.kzheart.klib.config.read
val settings = mapper.read<Settings>(document.root())
val sameType = mapper.read(document.root(), Settings::class)
val yaml = configDefaults<Settings>()Configs.load(Settings::class) 与 load(Settings::class, "config.yml") 同样可用。
configDefaults 要求每个主构造器参数都有真实默认值,并复用映射器的字段、别名、注释和校验规则;只返回文本,不写入现有文件。集合中的模型默认值也可生成;循环引用会被拒绝。
| 注解 / API | 作用 |
|---|---|
@Key |
字段映射 |
@Range |
数值范围 |
@Validate |
跨字段校验 |
@Comment |
用于显式 ConfigDefaults.yaml(Settings.class) 默认 YAML 生成,不会自动写入现有文件
|
- 校验失败沿用原子重载流程,保留旧值。
- 配置仍通过
ConfigDocument读取,不自动注入业务字段。
完整规则见 组件与注解。
内置映射覆盖常见配置模型:
| 类型 | 说明 |
|---|---|
| 字符串、布尔、字符、Java 数值类型 | — |
| 枚举 | 大小写不敏感 |
Duration |
ISO-8601,或 ms、s、m、h、d 组合,如 1m30s
|
Core 的 IntRange、DoubleRange
|
单个数字、"1-5"/"1~5"/"-5~-1" 文本,或带 min、max 的映射;上界小于下界时加载失败并报告位置 |
| 普通数组 | — |
List<T>、Set<T> 等集合 |
必须带泛型参数;原始集合类型没有元素类型信息,不支持 |
Map<String, T> |
键类型必须是 String
|
| 嵌套 Java POJO / Kotlin 配置模型 | 包括从父类继承的实例字段;Kotlin 构造器遵循上述规则 |
Java POJO 映射规则:
- 类型必须可实例化并有可访问的无参数构造器;Klib 会反射访问非公开构造器和字段。
-
static、transient和编译器生成字段会被跳过。 - YAML 未提供的字段,保留构造器或字段初始化器给出的默认值。
- YAML 提供了某字段但类型不匹配时,加载失败,不会静默转换。
出错信息包含来源文件、行列号和字段路径:
config.yml:12:5 (database.port): expected a number, got String
- YAML 本身语法错误时同样带上出错行列。
- 行列均从 1 开始。
- 源节点没有位置信息时,退回
config.yml:database.port: ...形式。
默认使用 UnknownKeyPolicy.WARN。YAML 中某个键在目标类型里找不到匹配字段或 Kotlin 构造器参数时,Klib 以 WARNING 记录一次:
config.yml:4:3 (database.prot): unknown configuration key is ignored; com.example.Settings$Database has no matching field
需要拒绝拼写错误时,在安装前显式选择 REJECT:
YamlConfigMapper mapper = new YamlConfigMapper(UnknownKeyPolicy.REJECT);
ConfigModule.install(this, mapper);ConfigModule.install(this, YamlConfigMapper(UnknownKeyPolicy.REJECT))-
WARN忽略未知键并保留字段默认值;同一映射器对同一来源、同一路径只告警一次。 -
REJECT抛出ConfigMappingException,包含来源、未知键路径及可用的 YAML 键行列;每次失败都会拒绝,不受告警去重影响。 - Java 和 Kotlin 的嵌套类型都遵循所选策略。重载失败保留旧对象,不发布半成品,也不执行变更监听器;首次加载失败则不会产生文档。
展开:未知键的判定边界
- 判定发生在类型化的 Java POJO / Kotlin 模型层级。
Map<String, T>的自由键不会被判为未知,但它的模型值、集合元素和数组元素会在各自层级检查。 -
@Key的别名按原样匹配;与static、transient或编译器生成字段同名的键仍视为有意忽略。 - 迁移系统写入的根键
_schema-version被忽略;同名键出现在嵌套模型中仍需有对应字段。 -
Object、自由键容器及自定义转换器负责解析的内容不做模型字段检查;转换器应自行校验其领域格式。
需要领域类型时,在安装前向 YamlConfigMapper 注册转换器,再把它传给 ConfigModule.install、配置文档或 Registry:
YamlConfigMapper mapper = new YamlConfigMapper()
.registerConverter(Endpoint.class, node -> {
String[] parts = String.valueOf(node.raw()).split(":", 2);
return new Endpoint(parts[0], Integer.parseInt(parts[1]));
});
ConfigModule.install(this, mapper);同一能力加载的文档共享该映射器。先完成策略选择和转换器注册,再开始并发加载或重载;转换器本身需要适应调用线程。
转换器要报错时,用公开方法 ConfigNode.mappingError(detail) 或 mappingError(detail, cause) 抛出。异常自动带上来源文件、行列号和当前路径,与内置错误格式一致。
ConfigDocument<Settings> config = configs().load(Settings.class);
config.onChange(() -> {
logger().info("配置已更新,debug=" + config.value().debug);
});| 触发方式 | 何时通知监听器 |
|---|---|
| 文件监听(生产文件源) | 只有内容修订发生改变时 |
显式 reload() / reloadAsync()
|
总是通知,即使内容没变 |
监听器在哪个线程运行:
-
KPlugin已提供同步调度能力,监听器排到 Bukkit 主线程。 - 没有安装调度能力的独立
Scope,在触发重载的线程直接运行监听器。
配置影响大量运行态资源时,最简单可靠的模式:
config.onChange(root::rebuild);重建后旧 ConfigDocument 已关闭,新的 setup 会创建新文档。不要在长期异步任务中缓存旧的 config.value() 或旧文档引用。
reload() 完成配置交换后就返回,但主线程监听器可能还在队列中。命令需要等所有监听器完成后再报告成功时:
CompletionStage<Void> completion = config.reloadAsync();reloadAsync() 的“异步”只指监听器:
- 读文件、解析、迁移、写回和映射都在调用线程同步完成,方法返回前新值已经交换。
- 返回的
CompletionStage只等待变更监听器(在KPlugin中排到主线程)执行完毕。 - 因此不要在主线程上把它当作“不阻塞的重载”。
- 解析阶段本身失败时,异常直接由
reload()抛出,或以已失败的CompletionStage返回。
它与 CommandBuiltins.standardAsync(..., config::reloadAsync, ...) 配合使用。监听器失败时:
- 不会回滚已经成功加载的新配置;
- 失败通过返回的
CompletionStage报告,同时以SEVERE级别写入日志(聚合异常包含每个失败监听器的堆栈); - 所以
config.onChange(root::rebuild)这类重建失败时,即使没人消费CompletionStage(例如文件监听触发的重载),控制台也能看到原因。
重载采用“候选值完整解析成功后再交换”。YAML 无效、字段类型错误或迁移失败时:
-
value()继续返回最近一次有效值; - 变更监听器不会执行;
- 手工
reload()抛出配置异常; - 文件监听触发的失败会记录,但不会让监听线程退出。
统一观测失败(注册本身归作用域持有,无需手工注销):
ConfigErrors.onReloadFailure(config, error ->
logger().error("配置重载失败,继续使用旧值", error));多个文件共同决定一组运行态数据时,用 ConfigSnapshot<T> 发布一个业务聚合对象。它只提供 prepare / commit 的内存边界,不接管文件读取、迁移、监听或业务校验。
展开:双文件聚合、不可变复制与失败边界
下面假设 pricesSource 和 selectionSource 是已配置好的 ConfigSource,默认初始聚合为 initialCatalog。Prices 的 prices 为 Map<String, Integer>,Selection 的 featured 为物品 ID:
YamlConfigMapper mapper = new YamlConfigMapper(UnknownKeyPolicy.REJECT);
ConfigSnapshot<Catalog> snapshot = new ConfigSnapshot<Catalog>(initialCatalog);
ConfigSnapshot.Candidate<Catalog> candidate = snapshot.prepare(() -> {
Prices prices = mapper.read(pricesSource.prepare().document().root(), Prices.class);
Selection selection = mapper.read(selectionSource.prepare().document().root(), Selection.class);
return new Catalog(prices.prices, selection.featured);
});
snapshot.commit(candidate);
// 每次业务操作只读取一次,之后复用同一个版本
Catalog current = snapshot.value();
int price = current.prices.get(current.featured);业务聚合负责跨文件引用检查和不可变复制;仅写 final 或 Kotlin val 不会冻结容器:
final class Catalog {
final Map<String, Integer> prices;
final String featured;
Catalog(Map<String, Integer> prices, String featured) {
Map<String, Integer> copy = new LinkedHashMap<String, Integer>(prices);
if (!copy.containsKey(featured)) {
throw new ConfigException("Featured item is missing: " + featured);
}
this.prices = Collections.unmodifiableMap(copy);
this.featured = featured;
}
}| 阶段 | 契约 |
|---|---|
prepare(builder) |
在调用线程执行;只读、解析、校验并复制候选。失败原样抛出,已发布引用不变 |
candidate.value() |
查看尚未发布的候选;业务必须保证发布前后都不再修改它 |
commit(candidate) |
一次原子引用交换;只接受此实例创建、且基线仍有效的候选 |
| 过期或重复提交 | 抛出 ConfigException,不覆盖新值;必须重新准备,框架不会自动重跑构建器 |
revision() |
成功提交后递增,供诊断使用;与分开调用的 value() 不构成原子读取对 |
- 上例只调用
ConfigSource.prepare(),不调用其PreparedConfig.commit();不会保存默认合并或迁移结果。不要在聚合构建器里用会提交源变更的load()、reload(),或修改已发布对象。 - 多次文件读取不是磁盘上的事务快照。若要求所有输入来自同一次部署,业务还须校验共同版本或在读取期间协调文件更新。
- 发布顺序以成功提交为准。准备期间若其他候选已提交,旧候选返回后也会被拒绝;即使新旧值相等,也不能绕过基线检查。
- 已持有旧聚合的读者可完成当前操作;下一次操作重新取
value(),避免多次读取跨版本混用。
严格全批与显式部分成功由业务选择:
- 严格全批:任一文件解析、字段检查或跨文件引用校验失败,就让
prepare失败,完整保留旧聚合;Registry也保持现有的全批失败策略。 - 显式部分成功:业务在构建器内逐文件捕获预期的配置异常,按来源收集 diagnostics,再对剩余集合重新验证 ID 唯一性、必需项和所有引用,最后复制为不可变聚合。剩余引用仍失效时必须拒绝整次发布。
- 不提供自动跳过错误文件、磁盘事务或副作用回滚。构造器、转换器、校验器和构建器应避免修改已发布对象或产生业务副作用;提交后的监听、资源重建与其他业务副作用由业务单独处理失败。
展开:UTF-8 解码、临时文件替换与持久性边界
- 配置文件使用严格 UTF-8 解码。
- 写回时先写入同目录临时文件,再优先使用原子替换。
- 文件系统不支持原子移动时,先保留
.bak备份再替换。 - 这些措施用于避免常规写入失败留下半成品;不会额外强制文件或目录元数据刷入物理存储,也不承诺突然断电或操作系统崩溃时的持久性。
通过 ConfigModule.install 的迁移提供器,为不同文档建立连续迁移链;需要严格策略或自定义转换器时,把 mapper 作为最后一个参数:
ConfigModule.install(
context().scope(),
getDataFolder().toPath(),
getClass().getClassLoader(),
"defaults",
path -> {
MigrationRunner migrations = new MigrationRunner();
if ("config.yml".equals(path)) {
migrations
.add(1, Migrations.rename("old-name", "name"))
.add(2, Migrations.rename(
"limits.old-max",
"limits.max"));
}
return migrations;
},
new YamlConfigMapper(UnknownKeyPolicy.REJECT));- 省略最后的
mapper参数时仍使用默认WARN;只需要映射器、不需要迁移时,使用install(scope, directory, loader, "defaults", mapper)。 - 配置中的
_schema-version记录当前版本。 - 迁移版本必须从当前版本开始逐级连续。缺少中间版本,或用户文件版本高于程序支持版本时,拒绝加载。
- 迁移应设计为幂等操作。
-
Migrations.rename保留节点相关注释;目标路径已存在时保留目标值并移除旧键。 - 默认配置合并也会保留用户已有值和注释,只补入缺少的默认项。
一类定义分散在多个 YAML 文件时,用 Registry<T>:
Registry<ArenaDefinition> arenas = Registry.open(
root,
getDataFolder().toPath().resolve("arenas"),
ArenaDefinition.class,
definition -> definition.id,
new YamlConfigMapper());
ArenaDefinition spawn = arenas.find("spawn").orElse(null);
arenas.onChange(() -> logger().info("竞技场定义已更新"));- 只读取目录中的
.yml和.yaml文件,以提取函数给出的 ID 组成不可变快照。 - 任一文件加载失败时保留完整旧快照,不会发布半成功结果。
- 同样支持文件监听和
ConfigErrors.onReloadFailure;变更监听器失败也以SEVERE级别记录。
YamlConfigDocument 和 Registry 实现 Core 的 DiagnosticSource。快照只包含来源、值类型、当前修订、
条目数、生命周期状态以及最近一次重载/监听器失败的异常类型,不包含配置对象或 YAML 正文,也不会在
Incident 发生时重新读取文件。开发者可显式把它们交给 Remote:
remoteLoggerBuilder
.contributor(new KlibDiagnosticContributor(config))
.contributor(new KlibDiagnosticContributor(registry));Remote 不会自动发现配置对象;是否采集这些上下文由插件开发者决定。接入与预算规则见 Remote。
对底层 YamlDocument 的节点调用 parent.createSection("transfer"),在已存在的映射内创建空子节并返回视图。
- 同名值会被替换;父节点和文档后续读取立即可见,
toYaml()包含新子节。 - 键按原样处理,
"a.b"是单个键,不会拆成路径。 - 当前节点缺失或不是映射时,抛出带来源与路径的
ConfigMappingException,不会创建未关联的子节。 - 替换会保留已有键节点及其注释;旧子视图仍引用旧节点,替换后需要重新读取。
- 这是内存中的文档修改,不会自动保存到文件,也不触发配置重载监听。
展开:用 ValueDocument 承载第三方解析器(JSON/TOML 等)得到的 Map
已用独立解析器得到 Map<String, ?> 时,可以创建 ValueDocument,直接保留原有标量类型,不必先序列化为 YAML。
- 它不读取文件、不解析 JSON/TOML,也不自动解码字符串。
- 解析策略与格式 writer 由调用方明确提供。
Map<String, Object> values = new LinkedHashMap<String, Object>();
values.put("title", "NPC");
values.put("settings", new LinkedHashMap<String, Object>());
ValueDocument document = ValueDocument.of("dialogue.json", values,
(path, section) -> jsonSectionWriter.write(path, section));
ConfigNode root = document.root();
ConfigNode settings = document.node("settings");
settings.createSection("transfer");
String text = settings.sectionText();path writer 回调(jsonSectionWriter 表示调用方实际选用的格式 writer,不是本模块提供的 JSON API):
- 首参是当前节点路径,根节点为
"";第二参是当前 mapping 的深层不可变 Map/List 快照。 - 相同值的两个节仍会分别传入各自路径,可用于定位原文件中的注释。
- 回调必须返回非 null 文本,异常直接传给调用方。
ValueDocument.of 的数据语义:
- 复制输入 Map/List 容器为文档自有的可变树,之后修改输入容器不影响文档。
- 标量保留原对象及类型,例如日期、布尔值和大整数。
- 嵌套 mapping 键必须是 String,不接受递归 Map/List 容器。
-
raw()返回深层不可变普通 Map/List 快照,不随文档修改而改变;标量对象本身不会额外克隆。 - null 和缺失键的
raw()都为 null,用exists()区分。
两种文档(YamlDocument、ValueDocument)共有的节点能力:
| 方法 | 行为 |
|---|---|
name() |
返回单个原样键,根返回空字符串;child("a.b") 的 name 为 "a.b",不按点号截断 |
node("a.b") |
仍按点分路径逐级读取;与 literal-key 的 child 分开使用 |
index(i) |
返回列表元素视图,路径为 list[i],name 为 [i]。非列表抛带来源/路径的 ConfigMappingException,越界抛 IndexOutOfBoundsException
|
sectionText() |
只接受存在的 mapping。ValueDocument 调用上述 writer;YamlDocument 用自身 YAML writer 渲染该子节并保留子节内注释,不把普通 Map.toString 当作格式文本 |
sameNode(other) |
比较节点身份,不比较内容(见下) |
createSection |
直接修改所在文档的父 mapping,同一父树的其它视图可见新值。替换前持有的旧子视图继续指向旧节点,需重新读取。不自动保存文件或派发 reload |
sameNode(other) 的身份规则:
- 必须属于同一文档;不同文档不可混比,也不暴露底层可变容器。
- YAML 比较 AST 节点;原值 mapping/list 比较容器引用;原值标量(含显式 null)比较父容器、原样键或索引与值引用。
- 两个值相等、路径文本相同或来源名相同,都不代表同一节点。
- 缺失节点和 null 参数始终返回 false。
节点 writer: 需要区分原节和同路径替换后的新节时,用命名工厂 ValueDocument.ofWithNodeWriter(sourceName, values, BiFunction<ConfigNode, Map<String,Object>, String>)。
- writer 第一个参数是本次
sectionText()的当前节点视图,第二个仍是深层不可变容器快照。 - 可以保存初始节点视图,用
sameNode关联所属格式的注释或其它元数据。 - 替换后的新节不会与初始节混同,仍持有的旧视图继续识别原节点;列表内映射与含点号、方括号的原样键也不依赖文本路径猜身份。
- 原有
of的 path writer 继续按原契约工作。两种 writer 的异常和 null 结果都直接传播,不返回伪造文本。
映射: YamlConfigMapper.read(document.root(), Settings.class) 可映射两种文档的 POJO、列表、Map、数组及自定义 converter。
- 默认未知键告警仍按来源和路径去重;显式
REJECT策略同样拒绝原值文档中的未知模型字段。 - 原值文档没有 YAML 行列标记,错误包含来源与节点路径;YAML 文档继续提供原行列信息。
- 原有
YamlDocument的解析、raw 转换、注释、默认值合并和 migration 行为保持原契约;新增后端不会把第三方格式的字符串或类型规范化规则强加给默认 YAML 文档。
| 业务入口 | 内部统一处理 |
|---|---|
| Java POJO / Kotlin 主构造器模型 | 缓存字段与构造计划、转换值、递归可空性和校验 |
ConfigNode |
隐藏 YAML AST 与原值树差异,保留路径、节点身份与来源诊断 |
ConfigDocument / Registry 重载 |
候选准备、有效值发布、作用域监听与失败聚合 |
这些实现均在 Kotlin 中;Java 模型与静态入口照常使用。Java 转换器、配置源、迁移 SAM 和注解保留平台类型、默认方法和反射目标,自定义实现无需改写成 Kotlin。
-
ConfigDocument、文件监听器、变更监听注册和Registry都属于创建它们的作用域;作用域关闭后不可继续重载。 -
value()和Registry.snapshot()发布的是完整替换后的快照;Java POJO、Kotlinvar和可变集合本身不会自动变为不可变对象:-
value()返回活对象,不是防御性拷贝,把它当只读快照用; - 修改字段不会写回 YAML,下一次 reload 后随实例被替换而丢失;
- 多线程访问以 reload 发布的新实例为准;需要长期持有时每次都重新调用
value(),不要缓存旧实例。
-
-
ConfigDocument和Registry的重载流程串行化;ConfigSnapshot允许并发准备,以候选基线和原子交换拒绝过期提交。 -
ConfigSnapshot不绑定Scope,也不持有监听器或后台任务。业务负责其生命周期,把提交和关闭放在同一个串行生命周期边界,避免作用域关闭后异步任务仍发布新值。 - 在
KPlugin中,变更监听器通常在 Bukkit 主线程执行,不要在监听器里做阻塞 I/O。 - 文件监听可能由后台线程触发。没有 Core 调度能力的测试或独立环境,必须自行提供合适的监听器执行器。
- 类路径默认文件是必需的,找不到对应资源时加载失败。
- YAML 数字采用严格语义:带前导零的整数会被拒绝;如果它是标识符,请加引号。
- 只修改
config.value()得到的对象不会写回 YAML。Config 是读取、合并与迁移系统,不是任意对象序列化器。 - 自动监听由文件系统事件驱动。外部编辑器可能产生多次事件,但相同修订不会重复通知。
- 只有一个局部功能受配置影响时,优先重建该子作用域,而不是手工逐项替换资源。