配置转换器
可用版本
本页描述 alpha 分支尚未发布的配置转换行为,自 v6.3.0 起可用。
ConfigConverter<T> 在声明 Java 值与普通配置数据之间双向转换。通过模块扫描包内的 @ConfigConverterFor 在配置构造前登记。文档存储和注册表协调桥接是内部实现,不是模块事务 API。
转换器 API
公开类型位于 com.ultikits.ultitools.config.convert。
public interface ConfigConverter<T> {
Object toPlain(T value, ConversionContext ctx) throws ConversionException;
T fromPlain(Object plain, ConversionContext ctx) throws ConversionException;
}ConversionContext 提供 file()、完整键不可变列表 path() 和包含泛型的 declaredType()。用 ctx.toPlain(nested) 转换嵌套运行值,ctx.fromPlain(nested, type) 转换嵌套声明类型。ConversionException 构造参数是原因、文件、完整路径、声明类型,可选底层原因;它提供定位转换失败信息。自 v6.3.0 起,传入的原因会以 (reason: ...) 显示在给服主的跳过警告里,键名像密钥时与值一起脱敏,所以原因要写给服主看:说明该设置期望什么。
普通数据与往返
自 v6.3.0 起,普通数据是 null、String、Boolean、Integer、Long、BigInteger、Double、这些数据组成的列表和字符串键映射。不能返回 Bukkit MemorySection 或任意 Java bean。UUID、枚举、BigDecimal 由内置转换器用文字表示。注册的 Bukkit ConfigurationSerializable 使用序列化别名和普通映射;读入 Object 字段的值保持普通数据,不自动构造 Bukkit 对象。
转换器对声明类型中集合和数组均无 null 元素的每个值 x 必须满足正向相等。类型化集合(包括 List<Object>)和引用数组(包括 Object[])写入时省略 null 元素,每个字段记录一条定位警告;从文件读取时,类型化集合里的 null 元素被跳过并给出定位警告,与 6.2 一致。映射的 null 值与整个 null 字段仍可往返。声明为 Object 的普通数据槽保持原样。反向相等适用于转换器输出的规范普通值 p(p = toPlain(x)):
fromPlain(toPlain(x)) equals x
toPlain(fromPlain(p)) equals p转换器可以接受非规范输入 q。其规范形式为 toPlain(fromPlain(q)),规范化必须稳定:
fromPlain(toPlain(fromPlain(q))) equals fromPlain(q)已批准的转换行为不变:数值转 String、数字文字转 int/float、"false" 转 boolean、重复元素转 Set。无需保留原始非规范表示。比较的是语义值,不是对象身份;普通数值按值比较。读时加十、写时不减十违反正向相等。面板映射叶编辑和三方重载依靠正向相等保持未改兄弟项。测试 null、嵌套值、转换器输出的普通值及接受的非规范输入。
注册与查找
自 v6.3.0 起,发现的转换器需要是 public 顶层类,带 public 无参构造器;扫描器不发现嵌套转换器。转换器在配置实体前实例化,不是 IoC bean;不能依赖注入或模块初始化副作用。放进 @UltiToolsModule(scanBasePackages = {...}) 的包。重复目标类型拒绝加载并列两个类。@ConfigConverterFor(value = T.class, exact = true) 只服务精确类型,默认 exact = false 也服务子类型。
查找顺序为:
- 显式非默认
@ConfigEntry(parser = X.class)使用旧适配器。 - 模块注册表:精确类、非 exact 父类链、非 exact 接口。
- 框架注册表,同样顺序。
- 泛型集合、映射、数组、枚举、
Object转换。 - 有登记别名的 Bukkit
ConfigurationSerializable后备转换。 - 无转换器:选中配置类型检查拒绝模块加载。
完整继承泛型在读取或创建任何选中配置文件前检查。配方值不支持时诊断结构如下(名称来自相关模块):
Module UltiRecipe, file config/recipes.yml, key "recipes": no config converter for ...RecipeDefinition (declared as java.util.Map<java.lang.String, ...RecipeDefinition>). Register one with @ConfigConverterFor in the module scan packages, or declare a supported plain-data type.实际消息列完整类型。声明类型可转换但整个值无效时使用最初声明默认值,无效列表/映射元素跳过并定位警告。未知运行对象在保存时拒绝且不改目标。绑定警告隐藏密钥形状值。
转换器示例
把以下值/字段成员放进 example.config 包的 MigrationExample 类(imports 放在外层类前)。转换器单独放进同一扫描包的 public 顶层文件 TokenConverter.java,见第二段;不能嵌套在外层类中。这是未发布分支内联片段,不是对已发布构件编译的引用。严格的单文字键映射使两条等式成立,包括 null 文字。
import java.util.Objects;
import com.ultikits.ultitools.annotations.ConfigEntry;
public static class Token {
public String text;
public Token(String text) { this.text = text; }
@Override public boolean equals(Object other) {
return other instanceof Token && Objects.equals(text, ((Token) other).text);
}
@Override public int hashCode() { return Objects.hashCode(text); }
}
@ConfigEntry(path = "token")
private Token token = new Token("hello");// TokenConverter.java: a separate top-level source file.
package example.config;
import java.util.LinkedHashMap;
import java.util.Map;
import example.config.MigrationExample.Token;
import com.ultikits.ultitools.config.convert.ConfigConverter;
import com.ultikits.ultitools.config.convert.ConfigConverterFor;
import com.ultikits.ultitools.config.convert.ConversionContext;
import com.ultikits.ultitools.config.convert.ConversionException;
@ConfigConverterFor(Token.class)
public class TokenConverter implements ConfigConverter<Token> {
public TokenConverter() { }
@Override public Object toPlain(Token value, ConversionContext ctx) {
if (value == null) { return null; }
Map<String, Object> plain = new LinkedHashMap<>();
plain.put("text", value.text);
return plain;
}
@Override public Token fromPlain(Object plain, ConversionContext ctx)
throws ConversionException {
if (plain == null) { return null; }
if (!(plain instanceof Map)) { throw failure(ctx); }
Map<?, ?> map = (Map<?, ?>) plain;
if (map.size() != 1 || !map.containsKey("text")
|| !(map.get("text") == null || map.get("text") instanceof String)) {
throw failure(ctx);
}
return new Token((String) map.get("text"));
}
private ConversionException failure(ConversionContext ctx) {
return new ConversionException("Expected only a text key containing text or null",
ctx.file(), ctx.path(), ctx.declaredType());
}
}旧解析器迁移
自 v6.3.0 起,六项公告首次带 @Deprecated(since = "6.3.0", forRemoval = true):ConfigEntry#parser()、interfaces.Parser、interfaces.ObjectConfigSerializer、interfaces.impl.pasers.ConfigParser、DefaultConfigParser、StringHashMapParser。公告下个 MINOR 6.4.0 删除,已发布的包名拼写 pasers 不变。
DefaultConfigParser 子类曾使用继承的配置节读取器和反射写入器。上述 Token 的旧声明可以是:
import java.util.Map;
public static class TokenParser extends
com.ultikits.ultitools.interfaces.impl.pasers.DefaultConfigParser {
@Override public Object parse(Object raw) {
Map<?, ?> values = (Map<?, ?>) super.parse(raw);
return new Token((String) values.get("text"));
}
}
@ConfigEntry(path = "token", parser = TokenParser.class)
private Token oldToken = new Token("hello");用转换器示例的字段和类替换旧字段/解析器。显式旧覆盖仍收到隔离 Bukkit 配置节(保留点号拆分),序列化输出仍须通过普通数据边界。默认 DefaultConfigParser.class 注解值改表示注册表转换,不增加任意自定义类原始透传例外。
完整映射键、文件保护、整份输出、原子/备份保存、重载和实体 accessor 删除见配置文件。存储已知限制仍记录在 #578 和 #580。