refactor: simplify ConfigLoader and enhance ConfigRepresenter

- Bump version to 0.0.2 and update timi-io dependency
- Refactor ConfigLoader with cleaner SnakeYAML integration
- Add LoaderOptions security settings (recursion limit, alias limit, size limit)
- Enhance ConfigRepresenter to skip null values and improve enum serialization
- Remove RawMapper inner class and rawMappingList in favor of direct field reflection
- Simplify converter mechanism with cleaner serialize/deserialize flow
- Update tests to match new simplified API
- Add .claude and temp files to .gitignore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Timi
2026-01-15 18:19:31 +08:00
parent a0984d0783
commit df0610c15f
5 changed files with 314 additions and 257 deletions

6
.gitignore vendored
View File

@ -37,5 +37,9 @@ build/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
test*.yaml test*.yaml
*_test.txt
/.claude
/CLAUDE.md
/AGENTS.md
tmpclaude-*

View File

@ -6,7 +6,7 @@
<groupId>com.imyeyu.config</groupId> <groupId>com.imyeyu.config</groupId>
<artifactId>timi-config</artifactId> <artifactId>timi-config</artifactId>
<version>0.0.1</version> <version>0.0.2</version>
<properties> <properties>
<maven.test.skip>true</maven.test.skip> <maven.test.skip>true</maven.test.skip>
@ -24,7 +24,7 @@
<dependency> <dependency>
<groupId>com.imyeyu.io</groupId> <groupId>com.imyeyu.io</groupId>
<artifactId>timi-io</artifactId> <artifactId>timi-io</artifactId>
<version>0.0.1</version> <version>0.0.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>

View File

@ -1,11 +1,9 @@
package com.imyeyu.config; package com.imyeyu.config;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava; import com.imyeyu.io.IOSize;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.java.ref.RefField;
import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
@ -13,51 +11,52 @@ import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.representer.Representer;
import java.io.File; import java.io.File;
import java.lang.reflect.Array; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* 配置加载器 - SnakeYAML 封装
* <ul>
* <li>基于 SnakeYAML 的配置加载器</li>
* <li>自动创建默认配置文件</li>
* <li>支持自定义类型转换器</li>
* <li>使用自定义 Representer 配置跳过 null 值以减少配置文件体积</li>
* <li>通过转换器解决 YAML 序列化 JavaFX Property 等问题</li>
* </ul>
*
* @author 夜雨 * @author 夜雨
* @version 2024-04-10 00:44 * @since 2026-01-12 12:03
*/ */
public class ConfigLoader<T> { public class ConfigLoader<T> {
private final File file; private final File file;
private final Yaml yaml; private final Yaml yaml;
private final Yaml rawYaml;
private final Class<T> clazz; private final Class<T> clazz;
private final List<RawMapper> rawMappingList = new ArrayList<>(); private final Map<Class<?>, BaseConverter<?, ?>> converters = new HashMap<>();
private final Map<Class<?>, BaseConverter<?, ?>> converterMap = new HashMap<>();
private T value; private T value;
private LinkedHashMap<String, Object> rawValue;
public ConfigLoader(String srcPath, Class<T> clazz) { /**
this(srcPath, srcPath, clazz); * 创建配置加载器,源路径和目标路径相同
*/
public ConfigLoader(String path, Class<T> clazz) {
this(path, path, clazz);
} }
/**
* 创建配置加载器
*
* @param srcPath 默认配置文件路径,通常为 classpath 资源
* @param distPath 配置文件路径
* @param clazz 配置类型
*/
public ConfigLoader(String srcPath, String distPath, Class<T> clazz) { public ConfigLoader(String srcPath, String distPath, Class<T> clazz) {
this.file = new File(distPath);
this.clazz = clazz; this.clazz = clazz;
LoaderOptions loaderOptions = new LoaderOptions(); // 如果配置文件不存在则复制默认配置
Constructor constructor = new Constructor(loaderOptions);
DumperOptions dumperOptions = new DumperOptions();
dumperOptions.setIndent(4);
dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
dumperOptions.setIndicatorIndent(4);
dumperOptions.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN);
dumperOptions.setIndentWithIndicator(true);
Representer representer = new ConfigRepresenter(dumperOptions);
yaml = new Yaml(constructor, representer, dumperOptions);
file = new File(distPath);
if (!file.exists()) { if (!file.exists()) {
try { try {
IO.resourceToDisk(getClass(), srcPath, distPath); IO.resourceToDisk(getClass(), srcPath, distPath);
@ -65,173 +64,222 @@ public class ConfigLoader<T> {
throw new TimiException(TimiCode.ERROR, "load default config error", e); throw new TimiException(TimiCode.ERROR, "load default config error", e);
} }
} }
// 配置 SnakeYAML
LoaderOptions loaderOptions = new LoaderOptions();
loaderOptions.setAllowRecursiveKeys(false); // 禁止递归键
loaderOptions.setMaxAliasesForCollections(50); // 限制别名数量
loaderOptions.setCodePointLimit((int) (IOSize.MB * 3)); // 限制 3 MB
DumperOptions dumperOptions = new DumperOptions();
dumperOptions.setIndent(4);
dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
dumperOptions.setIndicatorIndent(4);
dumperOptions.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN);
dumperOptions.setIndentWithIndicator(true);
// 使用自定义 Representer跳过 null 值以减少配置文件体积
Representer representer = new ConfigRepresenter(dumperOptions);
// 根据类型创建 Constructor
Constructor constructor;
if (clazz != null && !clazz.equals(Object.class)) {
constructor = new Constructor(clazz, loaderOptions);
} else {
constructor = new Constructor(loaderOptions);
}
this.yaml = new Yaml(constructor, representer, dumperOptions);
this.rawYaml = new Yaml(new Constructor(loaderOptions));
} }
/** 加载配置 */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public T load() { public T load() {
try { try {
rawValue = yaml.load(IO.toString(file).replaceAll("\t", " ")); String content = IO.toString(file);
String normalized = content.replaceAll("\t", " ");
if (clazz == null || clazz.equals(Object.class)) { if (converters.isEmpty()) {
value = (T) rawValue; // SnakeYAML 自动转换 - 使用指定类型构造
} else { value = yaml.load(normalized);
// 解析配置对象
rawMappingList.clear();
value = clazz.getDeclaredConstructor().newInstance();
deserializeToValue(value, rawValue, "");
}
return this.value;
} catch (Exception e) {
throw new TimiException(TimiCode.ERROR, "load config error", e);
}
}
public void dump() {
try {
serializeToRawValue();
IO.toFile(file, yaml.dump(rawValue).replaceAll("( ){4}", "\t"));
} catch (Exception e) {
throw new TimiException(TimiCode.ERROR, "dump config error", e);
}
}
public void addConverter(Class<?> type, BaseConverter<?, ?> converter) {
converterMap.put(type, converter);
}
public void removeConverter(Class<?> type) {
converterMap.remove(type);
}
@SuppressWarnings("unchecked")
private void serializeToRawValue() throws Exception {
for (int i = 0; i < rawMappingList.size(); i++) {
RawMapper rawMapper = rawMappingList.get(i);
String[] bukkit = rawMapper.rawStack.split("#");
Map<String, Object> raw = rawValue;
String key;
if (bukkit.length == 1) {
key = bukkit[0];
} else {
for (int j = 0; j < bukkit.length - 1; j++) {
raw = (Map<String, Object>) rawValue.get(bukkit[j]);
}
key = bukkit[bukkit.length - 1];
}
Object value = rawMapper.refField.getGetter().invoke(rawMapper.owner);
// 字段注解序列化
Converter fieldConverter = rawMapper.refField.getType().getAnnotation(Converter.class);
if (fieldConverter != null) {
BaseConverter<?, ?> converter = Ref.newInstance(fieldConverter.value());
raw.put(key, converter.serialize0(rawMapper.refField.getField(), value));
continue;
}
// 类型序列化
if (converterMap.containsKey(rawMapper.refField.getType())) {
BaseConverter<?, ?> converter = converterMap.get(rawMapper.refField.getType());
raw.put(key, converter.serialize0(rawMapper.refField.getField(), value));
continue;
}
// 枚举序列化
if (Enum.class.isAssignableFrom(rawMapper.refField.getType())) {
raw.put(key, rawMapper.refField.getGetter().invoke(rawMapper.owner).toString());
continue;
}
// 默认序列化
raw.put(key, rawMapper.refField.getGetter().invoke(rawMapper.owner));
}
}
@SuppressWarnings("unchecked")
private void deserializeToValue(Object object, Map<?, ?> map, String stack) throws Exception {
for (Map.Entry<?, ?> item : map.entrySet()) {
RefField refField = Ref.field(object.getClass(), item.getKey().toString());
if (!(item.getValue() instanceof Map<?, ?>)) {
// 缓存映射
RawMapper rawMapper = new RawMapper();
rawMapper.owner = object;
rawMapper.refField = refField;
rawMapper.rawStack = stack + item.getKey();
rawMappingList.add(rawMapper);
}
Class<?> type = refField.getType();
// 字段注解反序列化
Converter fieldConverter = refField.getType().getAnnotation(Converter.class);
if (fieldConverter != null) {
BaseConverter<?, ?> converter = Ref.newInstance(fieldConverter.value());
refField.getSetter().invoke(object, converter.deserialize0(refField.getField(), item.getValue()));
continue;
}
// 类型反序列化
if (converterMap.containsKey(type)) {
refField.getSetter().invoke(object, converterMap.get(type).deserialize0(refField.getField(), item.getValue()));
continue;
}
// 默认反序列化
if (item.getValue() instanceof Map<?, ?> valueMap) {
// 子级对象
Object newObject = type.getDeclaredConstructor().newInstance();
refField.getSetter().invoke(object, newObject);
deserializeToValue(newObject, valueMap, stack + item.getKey() + "#");
continue;
}
// 数组
if (item.getValue() instanceof List<?> valueList) {
// 数组
if (type.isArray()) {
Class<?> arrayType = type.getComponentType();
Object array = Array.newInstance(arrayType, valueList.size());
if (arrayType.isPrimitive()) {
// 基本类型数组
if (TimiJava.isNotEmpty(valueList)) {
Method method = Ref.getMethod(valueList.get(0).getClass(), arrayType.getName() + "Value");
for (int i = 0; i < valueList.size(); i++) {
Array.set(array, i, method.invoke(valueList.get(i)));
}
}
} else {
for (int i = 0; i < valueList.size(); i++) {
Array.set(array, i, arrayType.cast(valueList.get(i)));
}
}
refField.getSetter().invoke(object, array);
continue;
}
// 列表
refField.getSetter().invoke(object, valueList);
continue;
}
// 枚举
if (Enum.class.isAssignableFrom(type)) {
refField.getSetter().invoke(object, Ref.toType((Class<Enum<?>>) type, item.getValue().toString()));
continue;
}
refField.getSetter().invoke(object, item.getValue());
}
}
public T getValue() {
return value; return value;
} }
Object raw = rawYaml.load(normalized);
if (raw == null) {
value = null;
return null;
}
if (clazz == null || clazz.equals(Object.class)) {
value = (T) raw;
return value;
}
if (raw instanceof Map<?, ?> rawMap) {
value = mapToBean(rawMap, clazz);
return value;
}
value = (T) raw;
return value;
} catch (Exception e) {
throw new TimiException(TimiCode.ERROR, "load config error: " + file.getAbsolutePath(), e);
}
}
public Map<String, Object> getRawValue() { /** 保存配置 */
return rawValue; public void dump() {
try {
// 应用序列化转换器
Object toSerialize = applyConvertersOnDump(value);
// SnakeYAML 序列化
String yamlContent = yaml.dump(toSerialize);
// 空格转制表符
String toWrite = yamlContent.replaceAll("( ){4}", " ");
IO.toFile(file, toWrite);
} catch (Exception e) {
throw new TimiException(TimiCode.ERROR, "dump config error: " + file.getAbsolutePath(), e);
}
} }
/** /**
* @author 夜雨 * 添加自定义类型转换器
* @version 2024-04-10 00:45 * <p>
* 用于 YAML 不支持的类型序列化和反序列化,如 JavaFX Property 等
*
* @param type 字段类型
* @param converter 转换器
*/ */
private static class RawMapper { public void addConverter(Class<?> type, BaseConverter<?, ?> converter) {
converters.put(type, converter);
}
Object owner; private <K> K mapToBean(Map<?, ?> map, Class<K> targetClass) {
try {
RefField refField; K instance = targetClass.getDeclaredConstructor().newInstance();
applyMapToObject(map, instance);
String rawStack; return instance;
} catch (Exception e) {
throw new TimiException(TimiCode.ERROR, "map to bean error: " + targetClass.getName(), e);
}
}
private void applyMapToObject(Map<?, ?> map, Object target) throws IllegalAccessException {
Class<?> currentClass = target.getClass();
while (currentClass != null && currentClass != Object.class) {
for (Field field : currentClass.getDeclaredFields()) {
String fieldName = field.getName();
if (!map.containsKey(fieldName)) {
continue;
}
Object rawValue = map.get(fieldName);
Object converted = convertFieldValue(field, rawValue);
field.setAccessible(true);
field.set(target, converted);
}
currentClass = currentClass.getSuperclass();
}
}
private Object convertFieldValue(Field field, Object rawValue) {
BaseConverter<?, ?> converter = converters.get(field.getType());
if (converter != null) {
return converter.deserialize0(field, rawValue);
}
if (rawValue == null) {
return null;
}
Class<?> fieldType = field.getType();
if (fieldType.isEnum()) {
return toEnumValue(fieldType, rawValue.toString());
}
if (rawValue instanceof Map<?, ?> rawMap && shouldMapToBean(fieldType)) {
return mapToBean(rawMap, fieldType);
}
return convertScalarValue(fieldType, rawValue);
}
private boolean shouldMapToBean(Class<?> fieldType) {
if (fieldType.isPrimitive() || fieldType.isEnum()) {
return false;
}
if (fieldType.getName().startsWith("java.")) {
return false;
}
return !fieldType.isInterface();
}
private Object convertScalarValue(Class<?> fieldType, Object rawValue) {
if (fieldType.isInstance(rawValue)) {
return rawValue;
}
String text = rawValue.toString();
if (fieldType == String.class) {
return text;
}
if (fieldType == int.class || fieldType == Integer.class) {
return rawValue instanceof Number ? ((Number) rawValue).intValue() : Integer.parseInt(text);
}
if (fieldType == long.class || fieldType == Long.class) {
return rawValue instanceof Number ? ((Number) rawValue).longValue() : Long.parseLong(text);
}
if (fieldType == double.class || fieldType == Double.class) {
return rawValue instanceof Number ? ((Number) rawValue).doubleValue() : Double.parseDouble(text);
}
if (fieldType == float.class || fieldType == Float.class) {
return rawValue instanceof Number ? ((Number) rawValue).floatValue() : Float.parseFloat(text);
}
if (fieldType == short.class || fieldType == Short.class) {
return rawValue instanceof Number ? ((Number) rawValue).shortValue() : Short.parseShort(text);
}
if (fieldType == byte.class || fieldType == Byte.class) {
return rawValue instanceof Number ? ((Number) rawValue).byteValue() : Byte.parseByte(text);
}
if (fieldType == boolean.class || fieldType == Boolean.class) {
return rawValue instanceof Boolean ? rawValue : Boolean.parseBoolean(text);
}
if (fieldType == char.class || fieldType == Character.class) {
return text.isEmpty() ? null : text.charAt(0);
}
return rawValue;
}
@SuppressWarnings("unchecked")
private <E extends Enum<E>> E toEnumValue(Class<?> enumType, String value) {
Class<E> enumClass = (Class<E>) enumType;
for (E constant : enumClass.getEnumConstants()) {
if (constant.name().equalsIgnoreCase(value)) {
return constant;
}
}
return null;
}
/** 序列化时应用转换器 */
@SuppressWarnings("unchecked")
private <K> K applyConvertersOnDump(K obj) {
if (obj == null || converters.isEmpty()) {
return obj;
}
try {
// 创建副本
K copy = (K) obj.getClass().getDeclaredConstructor().newInstance();
Class<?> currentClass = obj.getClass();
while (currentClass != null && currentClass != Object.class) {
for (Field field : currentClass.getDeclaredFields()) {
field.setAccessible(true);
Object fieldValue = field.get(obj);
BaseConverter<?, ?> converter = converters.get(field.getType());
if (converter != null) {
// 应用转换器
Object yamlValue = converter.serialize0(field, fieldValue);
field.set(copy, yamlValue);
} else {
// 直接复制
field.set(copy, fieldValue);
}
}
currentClass = currentClass.getSuperclass();
}
return copy;
} catch (Exception e) {
throw new TimiException(TimiCode.ERROR, "converter error on dump", e);
}
} }
} }

View File

@ -1,18 +1,53 @@
package com.imyeyu.config; package com.imyeyu.config;
import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.representer.Representer;
/** /**
* 自定义 Representer - SnakeYAML 序列化增强
* <ul>
* <li>跳过 null 值字段,保持 YAML 简洁</li>
* <li>禁用类型标签,输出纯净的 YAML</li>
* <li>枚举序列化为字符串</li>
* </ul>
*
* @author 夜雨 * @author 夜雨
* @version 2024-05-01 14:03 * @since 2026-01-12 12:02
*/ */
public class ConfigRepresenter extends Representer { public class ConfigRepresenter extends Representer {
public ConfigRepresenter(DumperOptions options) { public ConfigRepresenter(DumperOptions options) {
super(options); super(options);
// null 值表示为空字符串
this.nullRepresenter = data -> representScalar(Tag.NULL, ""); this.nullRepresenter = data -> representScalar(Tag.NULL, "");
// 枚举转字符串(不带类型标签)
this.representers.put(Enum.class, data -> {
Enum<?> enumValue = (Enum<?>) data;
return representScalar(Tag.STR, enumValue.name());
});
}
@Override
protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) {
// 跳过 null 值,保持 YAML 简洁
if (propertyValue == null) {
return null;
}
return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
}
@Override
protected org.yaml.snakeyaml.nodes.MappingNode representJavaBean(java.util.Set<Property> properties, Object javaBean) {
Class<?> clazz = javaBean.getClass();
// 对于非 Java 标准库的类,使用 MAP 标签而不是类名标签
if (!clazz.getName().startsWith("java.") && !clazz.getName().startsWith("javax.")) {
if (this.classTags.get(clazz) == null) {
this.addClassTag(clazz, Tag.MAP);
}
}
return super.representJavaBean(properties, javaBean);
} }
} }

View File

@ -1,106 +1,76 @@
package test; package test;
import com.imyeyu.config.BaseConverter;
import com.imyeyu.config.ConfigLoader; import com.imyeyu.config.ConfigLoader;
import com.imyeyu.io.IO; import com.imyeyu.io.IO;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.File; import java.io.File;
import java.lang.reflect.Field;
import java.util.Map;
/** /**
* 测试 ConfigLoader - SnakeYAML 简洁封装
*
* @author 夜雨 * @author 夜雨
* @version 2024-04-25 11:13 * @version 2026-01-12
*/ */
public class TestConfig { public class TestConfig {
private static final String FILE_PATH = "test.yaml"; private static final String FILE_PATH = "test.yaml";
public <T> ConfigLoader<T> load(Class<T> clazz) { @Test
ConfigLoader<T> loader = new ConfigLoader<>(FILE_PATH, clazz); public void testSimpleLoad() {
loader.load(); // 删除旧文件,从资源重新创建
return loader;
}
public <T> ConfigLoader<T> reload(Class<T> clazz) {
IO.destroy(new File(FILE_PATH)); IO.destroy(new File(FILE_PATH));
return load(clazz);
// 创建加载器
ConfigLoader<Config> loader = new ConfigLoader<>("test.yaml", FILE_PATH, Config.class);
// 加载配置
Config config = loader.load();
// 验证加载正确
assert config.getStr().equals("str value");
assert config.getNum() == 123;
assert config.isBool() == true;
assert config.getEnumTest() == Config.EnumTest.TEST2;
System.out.println("✓ 加载测试通过");
} }
@Test @Test
public void testLoad() { public void testLoadAndDump() {
ConfigLoader<Object> loader = reload(null); // 测试加载和保存功能
loader.load(); IO.destroy(new File(FILE_PATH));
assert loader.getRawValue() != null; ConfigLoader<Config> loader = new ConfigLoader<>("test.yaml", FILE_PATH, Config.class);
}
@Test Config config = loader.load();
public void testRaw() { config.setStr("updated value");
ConfigLoader<Object> loader = reload(Object.class); config.setNum(999);
Map<String, Object> rawValue = loader.getRawValue(); config.setBool(false);
rawValue.put("str", "str value update");
loader.dump();
loader.load();
rawValue = loader.getRawValue();
assert rawValue.get("str").equals("str value update");
}
@Test
public void testObject() {
ConfigLoader<Config> loader = reload(Config.class);
Config value = loader.getValue();
assert value.getStr().equals("str value");
assert value.getNum() == 123;
assert value.getList().length == 4;
assert value.getObj() != null;
assert value.getObj().getObjStr().equals("test str");
assert value.getObj().getObjList().size() == 2;
value.setStr("dump object test");
value.getObj().getObjList().add(0, "new list item test");
loader.dump(); loader.dump();
value = loader.load(); // 重新加载验证值正确
assert value.getStr().equals("dump object test"); config = loader.load();
assert value.getObj().getObjList().get(0).equals("new list item test"); assert config.getStr().equals("updated value");
assert config.getNum() == 999;
assert config.isBool() == false;
System.out.println("✓ 加载和保存测试通过");
} }
@Test @Test
public void testEnum() { public void testEnum() {
ConfigLoader<Config> loader = reload(Config.class); IO.destroy(new File(FILE_PATH));
Config value = loader.getValue(); ConfigLoader<Config> loader = new ConfigLoader<>("test.yaml", FILE_PATH, Config.class);
assert value.getEnumTest() == Config.EnumTest.TEST2;
value.setEnumTest(Config.EnumTest.TEST1); Config config = loader.load();
assert config.getEnumTest() == Config.EnumTest.TEST2;
config.setEnumTest(Config.EnumTest.TEST1);
loader.dump(); loader.dump();
value = loader.load();
assert value.getEnumTest() == Config.EnumTest.TEST1;
}
@Test config = loader.load();
public void testConverter() { assert config.getEnumTest() == Config.EnumTest.TEST1;
IO.destroy(new File("test_converter.yaml"));
ConfigLoader<Config> loader = new ConfigLoader<>("test_converter.yaml", Config.class); System.out.println("✓ 枚举测试通过");
loader.addConverter(ConfigConverterTest.class, new BaseConverter<ConfigConverterTest, String>() {
@Override
protected String serialize(Field field, ConfigConverterTest configConverterTest) {
return configConverterTest.getValue();
}
@Override
protected ConfigConverterTest deserialize(Field field, String data) {
ConfigConverterTest configConverterTest = new ConfigConverterTest();
configConverterTest.setValue(data);
return configConverterTest;
}
});
Config value = loader.load();
assert value.getConverterTest().getValue().equals("converter value");
value.getConverterTest().setValue("converter test update");
loader.dump();
value = loader.load();
assert value.getConverterTest().getValue().equals("converter test update");
} }
} }