配置文件
UltiTools提供了优雅的单例模式的封装API,让你可以像操作对象一样操作配置文件。
创建 YAML 配置文件
首先,你需要在 resources 文件夹中创建一个 config 文件夹。按照你的需求放入你的插件配置文件。这些配置文件会被原封不动的放入UltiTools插件的集体配置文件夹中展示给用户。
操作配置文件
创建配置文件对象
根据你的配置文件的键值对结构,创建一个类,继承 AbstractConfigEntity 类。
package com.ultikits.docs.config;
import com.ultikits.ultitools.abstracts.AbstractConfigEntity;
import com.ultikits.ultitools.annotations.ConfigEntity;
import com.ultikits.ultitools.annotations.ConfigEntry;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Getter
@Setter
@ConfigEntity("some/path/to/config")
public class SomeConfig extends AbstractConfigEntity {
@ConfigEntry(path = "somepath", comment = "somecomment")
private boolean something = false;
@ConfigEntry(path = "someMapPath", comment = "somecomment2", parser = StringHashMapParser.class)
private Map<String, String> someMap = new HashMap<>();
public SomeConfig(String configFilePath) {
super(configFilePath);
}
}构造函数要求(自 v6.3.0 起)
构造函数保持廉价、无副作用;验证和保存准备可能构造临时实例。
@ConfigEntity
@ConfigEntity 注解用于标记一个配置文件的位置,需要一个字符串参数,用于指定配置文件在插件配置文件夹中的路径。通常这个路径与你在开发过程中resource文件夹目录中的路径是相同的。
但是这里的字符串也可以指向一个文件夹。如果你指定的是一个文件夹,则该文件夹下只有 .yml 文件会被加载为当前配置类,其余类型会被静默跳过。
@Getter
@Setter
@ConfigEntity("test") // 这里是一个文件夹
public class TestConfig extends AbstractConfigEntity {
@ConfigEntry(path = "testString")
private String testString = "test";
...
}注意
如果你指定的是一个文件夹,那么你需要确保文件夹下的所有配置文件都是可被同一配置类读取的。这里的检测不会检测子文件夹。
你可以通过 UltiToolsPlugin#getConfigs 方法来获取所有被加载的配置类。
List<TestConfig> configs = BasicFunctions.getInstance().getConfigs(TestConfig.class);或者你可以直接指定一个文件夹内的配置文件的路径来获取配置类。
TestConfig config = BasicFunctions.getInstance().getConfig("test/test1.yml", TestConfig.class);@ConfigEntry
@ConfigEntry 注解用于标记一个配置项,
自 v6.3.0 起,path 中的点仍表示嵌套路径,例如 chat.aliases。绑定映射里的键则完整保留,支持 g.m、o.O、wave.。6.2 已经拆开的文件原样读取,不自动合并;Map<String, String> 中形状为嵌套映射的值跳过并警告。
自 v6.3.0 起,服主把设置写成带点的扁平键时就是该设置:对 path = "features.chat",features.chat: false 这一行读作 false,与 6.2 经 Bukkit 读取时相同;点以任何方式拆分都算(a.b.c 写成 a.b: / c: 1 也可以)。框架不会再补一份嵌套写法:启动补键、save()、saveOperatorChange、saveOperatorMapEntry 和面板编辑都只写这一行。@ConditionalOnConfig 和 isPresentInFile 按同样规则读取。同一文件里一个设置写了两种形式(features.chat: false 和 features: / chat: true)时,即使值相同,也在模块代码运行前拒绝加载该模块,提示只写文件和设置,不写值。此规则只针对设置路径,绑定映射里的键仍完整保留。
字面 comment 在插入该键时写入。恰好是一个去除首尾空白后的语言 token(例如 comment = "{config.limit}")时,按框架当前 language 从模块语言目录解析。自 v6.3.0 起,框架只改写它能认出是自己写的注释行:该项的注释整体或末尾连续几行,与框架对模块 jar 自带任一语言目录中的文字、模块当前解析出的文字、原样 token,或 previousComments 中登记的旧版出厂文字写出的形式逐字节相同(该项的缩进、# 加文字)。服主在该项上方写的注释、改过的框架注释和所有字面注释逐字节保留。启动和 /ul reload 会把框架自己的注释行刷新为当前语言;保存、服主操作写入和面板编辑不改写任何注释。目录缺键时保留 token 并警告一次,写 YAML 注释前清理换行和控制字符。
模块修改了某个 token 注释在语言目录中的文字后,已升级的服务器上仍是旧文字。把旧文字登记下来,它就会继续随语言切换:
@ConfigEntry(path = "lock.timeout", comment = "{config.lock.timeout}",
previousComments = {"Lock timeout in seconds"})
private int lockTimeout = 30;自 v6.3.0 起,parser 保持默认即使用声明类型转换器注册表。显式非默认旧解析器收到的恰好是 6.2 给它们的输入:通过全新的 Bukkit YamlConfiguration#get 得到的隔离输入,保留 6.2 配置节拆分点号键的行为,并在根值、列表和映射内部反序列化 == 别名;显式使用旧解析器的 Object 字段也一样。这条输入路径不使用注册表转换器,输出仍经过普通数据边界并保留包装数值拓宽。Bukkit 自身的别名限制不变:Vector 坐标为整数时反序列化为 null,带小数部分时正常反序列化。六项相关声明在 6.3.0 首次带 forRemoval,公告删除版本为 6.4.0。新代码使用配置转换器,不再继承 DefaultConfigParser。
数值字段
自 v6.3.0 起,基本类型和包装类型使用相同数值转换规则。面板 JSON 中 long 范围内的整数使用 Long,超出 long 范围的整数以 BigInteger 精确保留;小数仍使用既有 Double 路径。整数收窄必须值精确且在范围内,'30' 等数值文字可以绑定整数。小数绑定 float/Float 时,读出的 float 最短可打印十进制必须与原十进制数值相同:0.03、1.50 可用,0.100000001 不可用。无效字段使用声明默认值并记录定位警告;更多十进制精度使用 double,float 接受不表示二进制精确。
int、long、Integer、Long 还可以驱动任务间隔和命令冷却,见配置绑定时间与配置绑定冷却。配置必须恰好注册一次,目录实体不能绑定。避免同时使用 @Range,绑定已有范围检查。
集合与 null
自 v6.3.0 起,完整继承泛型参与列表、集合、队列、映射、数组和枚举转换。无效集合/映射元素跳过并警告,整个字段形状无效时使用最初声明默认值。警告定位文件、键、位置、类型,并隐藏密钥形状的值。未知声明类型在读写任何配置文件前拒绝模块加载;登记转换器,不把原始映射偷偷传入自定义类。
整个 null 字段和映射中的 null 值仍可往返,基本类型 null 无效。类型化集合(包括 List<Object>)和引用数组(包括 Object[])写入时省略 null 元素,每个字段记录一条定位警告。读取时,文件中类型化集合里的 null 元素同样被跳过并给出定位警告,与 6.2 一致;声明为 Object 的普通数据槽保持普通数据。UUID 和枚举使用普通文字,注册的 Bukkit ConfigurationSerializable 使用别名映射。未显式使用旧解析器时,读进 Object 槽的 Bukkit 对象仍是普通映射。未知运行时 Java 对象拒绝保存,不触碰文件。
@Getter 和 @Setter
@Getter 和 @Setter 则为Lombok注解,用于自动生成 getter 和 setter 方法。
获取配置文件对象
继承了 UltiToolsPlugin 的主类中,有一个 getConfig 方法,用于获取配置文件对象。
你需要获取插件主类的实例,然后调用 getConfig 方法。
SomeConfig someConfig = SomePlugin.getInstance().getConfig(SomeConfig.class);然后,你就可以使用 getter 和 setter 方法来操作配置文件了。
boolean something = someConfig.getSomething();设置与保存
自 v6.3.0 起,服务器关闭时不会保存任何配置。代码做出需要写入文件的改动时,请调用 save()。
自 v6.3.0 起,服务器关闭、模块卸载或模块被替换时都不写任何配置。从未保存的模块改动以一条 WARNING 列出(只列文件和键,不列值),随后丢弃;ConfigManager#saveAll() 不再写入并已标记弃用。服务器运行期间服主对文件所做的修改不会因关闭而被改动。上次加载时无法读取或解析的文件改为提示一次。面板只确认触及的字段,无关的未保存字段仍未保存。
服务器运行时,初始化、重载和 ConfigManager 注册表操作限主线程。异步 void 操作警告且不执行;注册表 getter、JSON 读写警告并抛 IllegalStateException。面板更新、上传、重连回调整体排到主线程,执行后回复。实体监视器串行化持久化,但不保护模块自己异步修改字段;请安排在主线程修改和重载。
模块 getConfig(Class) 仍返回实体。实体旧的可变 Bukkit getConfig() 在 v6.3.0 通过维护者一次性兼容 carve-out 删除;它在 6.2.5 确实可用,第三方用量未知。用 isPresentInFile("entry.path") 查上次成功加载的存在性(null 和未声明键也算),它拆分点号,不能定位映射完整点号键。修改声明字段后调用 save()。
由谁写入配置
配置是给服主读取和编辑的。只在服主要求某项改动时写入,优先使用 saveOperatorChange 或 saveOperatorMapEntry(见保存配置文件)。 如果你的插件需要自行持久化数据,请改用数据存储。
注册配置文件
自动注册
因为UltiTools提供了自动注册功能,所以你无需手动注册配置文件,只需要在你的配置文件类上添加 @ConfigEntity 注解即可。
请查看这篇文章来了解更多关于自动注册的内容。
手动注册
你可以重写你的插件主类中的 getAllConfigs 方法来注册配置文件。 这条路径仅在插件主类未启用自动配置注册(@EnableAutoRegister 或 @UltiToolsModule,其 config 属性默认为 true)时才会生效:一旦启用,getAllConfigs 就不会被调用,即使你重写了它。@ConfigEntity 在两条路径下都是必需的,但它本身并不决定哪条路径生效。
@Override
public List<AbstractConfigEntity> getAllConfigs() {
return Collections.singletonList(new SomeConfig("some/path/to/config"));
}配置校验
从 v6.2.0 开始,UltiTools 提供了校验注解来防止无效的配置值。详情请参阅配置校验指南。
package com.ultikits.docs.config;
import com.ultikits.ultitools.abstracts.AbstractConfigEntity;
import com.ultikits.ultitools.annotations.ConfigEntity;
import com.ultikits.ultitools.annotations.ConfigEntry;
import com.ultikits.ultitools.annotations.config.NotEmpty;
import com.ultikits.ultitools.annotations.config.Range;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@ConfigEntity("config/config.yml")
public class MyConfig extends AbstractConfigEntity {
@Range(min = 1, max = 100)
@ConfigEntry(path = "maxHomes", comment = "Maximum homes per player (1-100)")
private int maxHomes = 5;
@NotEmpty
@ConfigEntry(path = "serverName", comment = "Server display name")
private String serverName = "My Server";
public MyConfig(String configFilePath) {
super(configFilePath);
}
}可用的校验注解:@Range、@NotEmpty、@Size、@Pattern(来自 com.ultikits.ultitools.annotations.config 包)。
保存配置文件
写入约定
自 v6.3.0 起,框架绝不覆盖服主写的配置,除非服主要求了这项改动。代码可以写什么,取决于文件归谁所有:
| 文件 | 代码可以写入 |
|---|---|
模块自带的配置文件(config.yml、spawn.yml) | 文件不存在时首次创建;补入文件缺少的声明键和框架自己的注释,只插入;服主通过命令、面板或 GUI 明确要求修改的那一项;语言切换后,仍与出厂文字相同的值重新渲染 |
| 服主自己创建的文件(礼包、菜单) | 服主执行创建操作时创建文件;编辑时只写编辑的部分 |
除此之外一律不写:不在关闭、卸载或替换时保存,不修复无效值,不规整排版,也不清理 6.2 按点号拆开的映射键。
所有写入都经过同一个写入闸门。每次写入声明自己拥有的键;渲染后,这些键之外的每个字节必须与读取时相同,且文件仍须是读取时的内容。否则不写入:一条 WARNING 列出文件、键和原因,不列值,程序继续使用内存中的值。
官方语言文件是唯一的例外。它们归框架所有,升级时可能被替换;要定制文字,请复制官方文件并改名,编辑副本,再在主配置中选择它(见国际化)。
save()
自 v6.3.0 起,save() 只写模块自上次加载或保存以来改过的设置,并且只在文件该处仍是读取时的值时才写。声明为 Map 的设置按条目写入;列表,以及 Location、Vector 这类 Bukkit 值,要么整体写入,要么不写。保存从不补入文件缺少的键,从不改写注释,也从不删除模块没有删除的映射条目。
因此,服主在磁盘上改过的值、删掉的键,以及框架无法使用的值(interval: 3O0)都不会被覆盖。模块的改动留在内存中,一条 WARNING 列出该键。比较依据是上次读取的文字:服主不经 /ul reload 直接在磁盘上改过某个设置后,即使值不变(例如把 64 改成 64.0),模块对该设置的下一次改动也不会写入,直到重载重新读取文件。没有内容可写的保存不改动文件字节和修改时间。
服主命令
按服主要求只修改一项内容的命令,使用 v6.3.0 新增的两个方法之一。服主的请求即表示同意:在指定的键处,模块的值替换文件中的内容,其余一律不写。
// /setspawn:只写六个位置设置
config.setSpawn(player.getLocation());
config.saveOperatorChange("spawn.location.world", "spawn.location.x", "spawn.location.y",
"spawn.location.z", "spawn.location.yaw", "spawn.location.pitch");
// /autoreply add <name>:只写一个映射条目,服主手加的条目保留
config.getRules().put(name, rule);
try {
config.saveOperatorMapEntry("autoreply.rules", name);
} catch (ConfigWriteRefusedException refused) {
config.getRules().remove(name); // 让内存与文件保持一致
sender.sendMessage("未保存:" + refused.getReason());
}用 saveOperatorChange 指定整个映射设置会整体写入,并丢掉服主手加的条目;只改一个条目的命令应使用 saveOperatorMapEntry。被拒绝时抛出 com.ultikits.ultitools.config.ConfigWriteRefusedException,它是 IOException,getReason() 说明原因、不含任何值:请回复服主未保存及原因,并撤回内存中的改动,使运行状态与文件一致。路径不是已声明的配置项时抛 IllegalArgumentException。两个方法都在服务器主线程执行。
被拒绝的写入
文件无法读取或解析、使用了 YAML 锚点、别名或合并键、读取后被改动,或者排版无法被渲染器逐字节写回时,写入会被拒绝。排版引起的拒绝会给出要修改的行号,例如 the file's layout outside the keys this write owns would change (line 16)。
会使该文件所有写入都被拒绝的排版包括:只含空格的行、值后面的行尾空格、用多个空格对齐的行内注释、冒号后多于一个空格、流式方括号内侧的空格([ a ])、--- 或 ... 标记、后面跟空行的块标量、缩进比下面的键更深的注释、同一文件中的两种缩进宽度,以及混用的换行符。框架和各模块自带的文件都没有这类排版。服主改掉警告指出的那一行(如果原因是“文件在读取后已被改动”,则先执行 /ul reload),再重做一次改动即可。
0 字节文件、只有空行的文件和只含行首注释的文件视为空文件:启动时补入声明的键。只含空格的文件、含缩进注释的纯注释文件和只含 BOM 的文件按排版拒绝。
模块代管的服主文件
自 v6.3.0 起,com.ultikits.ultitools.config.OperatorFiles 用于在服主明确编辑时,写入模块代服主管理的 YAML 文件,例如礼包文件。它只写指定的键,经过同一个写入闸门,并且只在文件仍是读取时的字节时写入:
OperatorFiles.Snapshot snapshot = OperatorFiles.read(kitFile);
Map<List<String>, Object> edit = new LinkedHashMap<>();
edit.put(Arrays.asList(kitName, "items"), serializedItems); // 只能是普通数据
OperatorFiles.WriteResult result = OperatorFiles.write(snapshot, edit);结果为 WRITTEN、UNCHANGED、FILE_CHANGED(read 之后文件被改动)或 REFUSED(排版或锚点问题,闸门会记一条 WARNING)。列表中的每一项是一个完整的键,名字里含 . 也仍是一个键。OperatorFiles 从不创建、删除或重命名文件,也不用于 @ConfigEntity 文件。
原子替换
单个列表项的注释仅在列表长度不变时保留;Bukkit 则完全不保留列表项注释。
先强制同步同目录临时文件,再原子替换。仅不支持原子移动、EBUSY/跨设备或允许的临时创建拒绝走备份后原地写。自 v6.3.0 起,备份文件名为 <file>.ultitools-backup-<16 位十六进制>,在打开目标前同步;本次运行已为同一文件写过、且内容未变的备份,先从当前目标通过同步临时文件和原子替换刷新。服主自己的 <file>.bak 不会被读取、写入或删除。备份失败保持目标和旧备份;之后原地写失败可能留下部分目标,但完整备份保留。只有成功严格加载当前文件、且备份内容仍与写入时一致,才删除备份,不自动还原。
不能读取、不能解析或非 UTF-8 文件在所有实体写入路径受保护。初次失败用默认值,并以一条 SEVERE 列出文件和安全原因,不泄漏源码片段。自 v6.3.0 起,重载这样的文件会保留运行字段、不改动文件,并抛出列出文件和同一安全原因的 ConfigurationException;/ul reload <模块> 随之回复该模块重载失败及原因。成功加载才解除保护。验证在默认值、注释和面板持久化前执行。
注册批次等所有选定实体绑定验证完成才写入;拒绝批次不改文件。接受后各文件独立保存,经过写入闸门,并且只在文件仍是注册时读取的字节时写入。多文件面板先验证并暂存全部文件,然后提交且共同确认,普通进程内拒绝回滚;持久存储故障可能阻止恢复,移动中间崩溃不是安全多文件事务。
面板编辑同样是全有或全无:校验拒绝某个值,或文件写入失败时,被触及的字段恢复为原值,文件字节不变,面板收到失败结果,重试即可保存该编辑。
面板映射叶项按真实完整文件键和整个字段声明转换器处理。唯一变更保存,歧义或不存在变更拒绝整个请求并定位路径;未变显示项不算编辑。无关待保存内存和独立磁盘兄弟项保持,回复格式不变。
自 v6.3.0 起,面板编辑经写入闸门只写它指定的键:在这些键处替换服主改过的值(即服主的同意),文件的其余每个字节保持不变。闸门拒绝的文件会收到注明原因的错误回复。编辑 Location 这类 Bukkit 值中的一个字段时,写入的是文件中原样的整个值、只改该字段,其余字段保持服主写的文字(y: 64 不变),并且只在文件仍是读取时的值时写入;否则回复要求先重载。
重载配置文件
SomePlugin.getConfigManager().reloadConfigs(SomePlugin.getInstance());自 v6.3.0 起,重载比较上次有效基线、运行字段、传入文件。内存独有修改保持且脏,磁盘独有采用,冲突磁盘胜并安全定位警告。映射按完整键递归,列表和标量原子处理。服主从文件中删掉的整个字段使用声明默认值,并警告一次列出该键;但模块自上次加载或保存后改过该字段时保留模块的值。两种情况都不写文件。失败的重载是全有或全无的:重载失败时(例如校验拒绝某个值),内存与调用前完全一致并重新抛出异常。被拒绝的值因此不会留在字段里,被下一次重载当作覆盖在已修正文件之上的未保存修改保留下来;修正文件后再次重载即可。文件无法读取或解析时同样如此:重载抛出列出该文件的 ConfigurationException。模块自己调用 reload() 且只捕获 IOException 的,应同时捕获 ConfigurationException 并在自己的回复里报告。磁盘该映射未变时保留内存独有顺序,重载不写入它;这不是并发映射插入顺序策略。
/ul reload 重建模块语言之后、模块自己的重载钩子之前,框架经写入闸门、按重新读取的文件,把它能认出的自己的注释行改写为新语言,不写任何值、键或其它注释行。
自 v6.3.0 起,新版本副本替换旧副本时不保存旧副本的任何配置。新副本按文件现状读取;激活后警告一次,列出旧副本被丢弃的文件和键。卸载释放注册表所有者,不写入任何内容。
已知限制
自 v6.3.0 起,#578 记录特殊锚定容器、复杂符号链接路径、Unicode 风格定位成本和直接别名 token 注释所有权。别名注释可能影响源锚并重复写入。#580 记录首子键前有注释的有效块锚被拒绝;保护保留字节,不表示值可以读取。#545 仍是多文件崩溃持久化限制。