Files
timi-config/src/main/java/com/imyeyu/config/ConfigLoader.java
Timi df0610c15f 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>
2026-01-15 18:19:31 +08:00

286 lines
8.9 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.imyeyu.config;
import com.imyeyu.io.IO;
import com.imyeyu.io.IOSize;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;
import java.io.File;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* 配置加载器 - SnakeYAML 封装
* <ul>
* <li>基于 SnakeYAML 的配置加载器</li>
* <li>自动创建默认配置文件</li>
* <li>支持自定义类型转换器</li>
* <li>使用自定义 Representer 配置跳过 null 值以减少配置文件体积</li>
* <li>通过转换器解决 YAML 序列化 JavaFX Property 等问题</li>
* </ul>
*
* @author 夜雨
* @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 Map<Class<?>, BaseConverter<?, ?>> converters = new HashMap<>();
private T value;
/**
* 创建配置加载器,源路径和目标路径相同
*/
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;
// 如果配置文件不存在则复制默认配置
if (!file.exists()) {
try {
IO.resourceToDisk(getClass(), srcPath, distPath);
} catch (Exception 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")
public T load() {
try {
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 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);
}
}
/**
* 添加自定义类型转换器
* <p>
* 用于 YAML 不支持的类型序列化和反序列化,如 JavaFX Property 等
*
* @param type 字段类型
* @param converter 转换器
*/
public void addConverter(Class<?> type, BaseConverter<?, ?> converter) {
converters.put(type, converter);
}
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);
}
}
}