- 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>
286 lines
8.9 KiB
Java
286 lines
8.9 KiB
Java
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);
|
||
}
|
||
}
|
||
}
|