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 ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|
||||||
test*.yaml
|
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>
|
<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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user