Skip to content
Kz edited this page Oct 17, 2026 · 17 revisions

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: 3306

2. 创建配置类型。

  • 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 配置模型

原生 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 级别记录。

Remote 排障快照

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,不会创建未关联的子节。
  • 替换会保留已有键节点及其注释;旧子视图仍引用旧节点,替换后需要重新读取。
  • 这是内存中的文档修改,不会自动保存到文件,也不触发配置重载监听。

原值文档与格式 writer

展开:用 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 与 Kotlin 共用的配置流程

业务入口 内部统一处理
Java POJO / Kotlin 主构造器模型 缓存字段与构造计划、转换值、递归可空性和校验
ConfigNode 隐藏 YAML AST 与原值树差异,保留路径、节点身份与来源诊断
ConfigDocument / Registry 重载 候选准备、有效值发布、作用域监听与失败聚合

这些实现均在 Kotlin 中;Java 模型与静态入口照常使用。Java 转换器、配置源、迁移 SAM 和注解保留平台类型、默认方法和反射目标,自定义实现无需改写成 Kotlin。

线程与生命周期

  • ConfigDocument、文件监听器、变更监听注册和 Registry 都属于创建它们的作用域;作用域关闭后不可继续重载。
  • value() 和 Registry.snapshot() 发布的是完整替换后的快照;Java POJO、Kotlin var 和可变集合本身不会自动变为不可变对象:
    • value() 返回活对象,不是防御性拷贝,把它当只读快照用;
    • 修改字段不会写回 YAML,下一次 reload 后随实例被替换而丢失;
    • 多线程访问以 reload 发布的新实例为准;需要长期持有时每次都重新调用 value(),不要缓存旧实例。
  • ConfigDocument 和 Registry 的重载流程串行化;ConfigSnapshot 允许并发准备,以候选基线和原子交换拒绝过期提交。
  • ConfigSnapshot 不绑定 Scope,也不持有监听器或后台任务。业务负责其生命周期,把提交和关闭放在同一个串行生命周期边界,避免作用域关闭后异步任务仍发布新值。
  • 在 KPlugin 中,变更监听器通常在 Bukkit 主线程执行,不要在监听器里做阻塞 I/O。
  • 文件监听可能由后台线程触发。没有 Core 调度能力的测试或独立环境,必须自行提供合适的监听器执行器。

常见坑

  • 类路径默认文件是必需的,找不到对应资源时加载失败。
  • YAML 数字采用严格语义:带前导零的整数会被拒绝;如果它是标识符,请加引号。
  • 只修改 config.value() 得到的对象不会写回 YAML。Config 是读取、合并与迁移系统,不是任意对象序列化器。
  • 自动监听由文件系统事件驱动。外部编辑器可能产生多次事件,但相同修订不会重复通知。
  • 只有一个局部功能受配置影响时,优先重建该子作用域,而不是手工逐项替换资源。

相关页面

  • Core:配置文档的作用域与主线程调度来源。
  • Lang:使用同一配置基础设施维护语言文件。
  • Command:内置 reload 命令可等待 reloadAsync()。
  • 组件与注解:@ConfigFile、@Key、@Range、@Validate、@Comment 的完整规则。

Clone this wiki locally