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:
6
.gitignore
vendored
6
.gitignore
vendored
@ -37,5 +37,9 @@ build/
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
|
||||
test*.yaml
|
||||
*_test.txt
|
||||
/.claude
|
||||
/CLAUDE.md
|
||||
/AGENTS.md
|
||||
tmpclaude-*
|
||||
|
||||
4
pom.xml
4
pom.xml
@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.imyeyu.config</groupId>
|
||||
<artifactId>timi-config</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<version>0.0.2</version>
|
||||
|
||||
<properties>
|
||||
<maven.test.skip>true</maven.test.skip>
|
||||
@ -24,7 +24,7 @@
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.io</groupId>
|
||||
<artifactId>timi-io</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<version>0.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
package com.imyeyu.config;
|
||||
|
||||
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.TimiException;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.java.ref.RefField;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
@ -13,51 +11,52 @@ import org.yaml.snakeyaml.constructor.Constructor;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 配置加载器 - SnakeYAML 封装
|
||||
* <ul>
|
||||
* <li>基于 SnakeYAML 的配置加载器</li>
|
||||
* <li>自动创建默认配置文件</li>
|
||||
* <li>支持自定义类型转换器</li>
|
||||
* <li>使用自定义 Representer 配置跳过 null 值以减少配置文件体积</li>
|
||||
* <li>通过转换器解决 YAML 序列化 JavaFX Property 等问题</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2024-04-10 00:44
|
||||
* @since 2026-01-12 12:03
|
||||
*/
|
||||
public class ConfigLoader<T> {
|
||||
|
||||
private final File file;
|
||||
private final Yaml yaml;
|
||||
private final Yaml rawYaml;
|
||||
private final Class<T> clazz;
|
||||
private final List<RawMapper> rawMappingList = new ArrayList<>();
|
||||
private final Map<Class<?>, BaseConverter<?, ?>> converterMap = new HashMap<>();
|
||||
private final Map<Class<?>, BaseConverter<?, ?>> converters = new HashMap<>();
|
||||
|
||||
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) {
|
||||
this.file = new File(distPath);
|
||||
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()) {
|
||||
try {
|
||||
IO.resourceToDisk(getClass(), srcPath, distPath);
|
||||
@ -65,173 +64,222 @@ public class ConfigLoader<T> {
|
||||
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")
|
||||
public T load() {
|
||||
try {
|
||||
rawValue = yaml.load(IO.toString(file).replaceAll("\t", " "));
|
||||
|
||||
if (clazz == null || clazz.equals(Object.class)) {
|
||||
value = (T) rawValue;
|
||||
} else {
|
||||
// 解析配置对象
|
||||
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() {
|
||||
String content = IO.toString(file);
|
||||
String normalized = content.replaceAll("\t", " ");
|
||||
if (converters.isEmpty()) {
|
||||
// SnakeYAML 自动转换 - 使用指定类型构造
|
||||
value = yaml.load(normalized);
|
||||
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;
|
||||
|
||||
RefField refField;
|
||||
|
||||
String rawStack;
|
||||
private <K> K mapToBean(Map<?, ?> map, Class<K> targetClass) {
|
||||
try {
|
||||
K instance = targetClass.getDeclaredConstructor().newInstance();
|
||||
applyMapToObject(map, instance);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,53 @@
|
||||
package com.imyeyu.config;
|
||||
|
||||
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.representer.Representer;
|
||||
|
||||
/**
|
||||
* 自定义 Representer - SnakeYAML 序列化增强
|
||||
* <ul>
|
||||
* <li>跳过 null 值字段,保持 YAML 简洁</li>
|
||||
* <li>禁用类型标签,输出纯净的 YAML</li>
|
||||
* <li>枚举序列化为字符串</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2024-05-01 14:03
|
||||
* @since 2026-01-12 12:02
|
||||
*/
|
||||
public class ConfigRepresenter extends Representer {
|
||||
|
||||
public ConfigRepresenter(DumperOptions options) {
|
||||
super(options);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,106 +1,76 @@
|
||||
package test;
|
||||
|
||||
import com.imyeyu.config.BaseConverter;
|
||||
import com.imyeyu.config.ConfigLoader;
|
||||
import com.imyeyu.io.IO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 测试 ConfigLoader - SnakeYAML 简洁封装
|
||||
*
|
||||
* @author 夜雨
|
||||
* @version 2024-04-25 11:13
|
||||
* @version 2026-01-12
|
||||
*/
|
||||
public class TestConfig {
|
||||
|
||||
private static final String FILE_PATH = "test.yaml";
|
||||
|
||||
public <T> ConfigLoader<T> load(Class<T> clazz) {
|
||||
ConfigLoader<T> loader = new ConfigLoader<>(FILE_PATH, clazz);
|
||||
loader.load();
|
||||
return loader;
|
||||
}
|
||||
|
||||
public <T> ConfigLoader<T> reload(Class<T> clazz) {
|
||||
@Test
|
||||
public void testSimpleLoad() {
|
||||
// 删除旧文件,从资源重新创建
|
||||
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
|
||||
public void testLoad() {
|
||||
ConfigLoader<Object> loader = reload(null);
|
||||
loader.load();
|
||||
assert loader.getRawValue() != null;
|
||||
}
|
||||
public void testLoadAndDump() {
|
||||
// 测试加载和保存功能
|
||||
IO.destroy(new File(FILE_PATH));
|
||||
ConfigLoader<Config> loader = new ConfigLoader<>("test.yaml", FILE_PATH, Config.class);
|
||||
|
||||
@Test
|
||||
public void testRaw() {
|
||||
ConfigLoader<Object> loader = reload(Object.class);
|
||||
Map<String, Object> rawValue = loader.getRawValue();
|
||||
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");
|
||||
Config config = loader.load();
|
||||
config.setStr("updated value");
|
||||
config.setNum(999);
|
||||
config.setBool(false);
|
||||
loader.dump();
|
||||
|
||||
value = loader.load();
|
||||
assert value.getStr().equals("dump object test");
|
||||
assert value.getObj().getObjList().get(0).equals("new list item test");
|
||||
// 重新加载验证值正确
|
||||
config = loader.load();
|
||||
assert config.getStr().equals("updated value");
|
||||
assert config.getNum() == 999;
|
||||
assert config.isBool() == false;
|
||||
|
||||
System.out.println("✓ 加载和保存测试通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnum() {
|
||||
ConfigLoader<Config> loader = reload(Config.class);
|
||||
Config value = loader.getValue();
|
||||
assert value.getEnumTest() == Config.EnumTest.TEST2;
|
||||
value.setEnumTest(Config.EnumTest.TEST1);
|
||||
IO.destroy(new File(FILE_PATH));
|
||||
ConfigLoader<Config> loader = new ConfigLoader<>("test.yaml", FILE_PATH, Config.class);
|
||||
|
||||
Config config = loader.load();
|
||||
assert config.getEnumTest() == Config.EnumTest.TEST2;
|
||||
|
||||
config.setEnumTest(Config.EnumTest.TEST1);
|
||||
loader.dump();
|
||||
value = loader.load();
|
||||
assert value.getEnumTest() == Config.EnumTest.TEST1;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConverter() {
|
||||
IO.destroy(new File("test_converter.yaml"));
|
||||
config = loader.load();
|
||||
assert config.getEnumTest() == Config.EnumTest.TEST1;
|
||||
|
||||
ConfigLoader<Config> loader = new ConfigLoader<>("test_converter.yaml", Config.class);
|
||||
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");
|
||||
System.out.println("✓ 枚举测试通过");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user