diff --git a/.gitignore b/.gitignore
index c6d98d1..5ff6309 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,98 +1,38 @@
-# ---> JetBrains
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# AWS User-specific
-.idea/**/aws.xml
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn. Uncomment if using
-# auto-import.
-# .idea/artifacts
-# .idea/compiler.xml
-# .idea/jarRepositories.xml
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-# *.iml
-# *.ipr
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
-*.iws
-
-# IntelliJ
-out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# SonarLint plugin
-.idea/sonarlint/
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-# ---> Maven
target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-# https://github.com/takari/maven-wrapper#usage-without-binary-jar
-.mvn/wrapper/maven-wrapper.jar
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
-# Eclipse m2e generated files
-# Eclipse Core
-.project
-# JDT-specific (Eclipse Java Development Tools)
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..a0e1fc4
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..27ee5de
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ com.imyeyu.config
+ timi-config
+ 0.0.1
+
+
+ true
+ 21
+ 21
+ UTF-8
+
+
+
+
+ org.yaml
+ snakeyaml
+ 2.2
+
+
+ com.imyeyu.io
+ timi-io
+ 0.0.1
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.3
+ test
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ test
+
+
+
diff --git a/src/main/java/com/imyeyu/config/BaseConverter.java b/src/main/java/com/imyeyu/config/BaseConverter.java
new file mode 100644
index 0000000..80a4f9e
--- /dev/null
+++ b/src/main/java/com/imyeyu/config/BaseConverter.java
@@ -0,0 +1,36 @@
+package com.imyeyu.config;
+
+import java.lang.reflect.Field;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-12 00:44
+ */
+public abstract class BaseConverter {
+
+ @SuppressWarnings("unchecked")
+ K serialize0(Field field, Object t) {
+ return serialize(field, (T) t);
+ }
+
+ @SuppressWarnings("unchecked")
+ T deserialize0(Field field, Object data) {
+ return deserialize(field, (K) data);
+ }
+
+ /**
+ * 当组件值需要写入配置时的转换,默认直接 toString
+ *
+ * @param t 组件值类型
+ * @return 配置值
+ */
+ protected abstract K serialize(Field field, T t);
+
+ /**
+ * 当获取配置并即将设置到组件时调用
+ *
+ * @param data 配置值
+ * @return 返回值
+ */
+ protected abstract T deserialize(Field field, K data);
+}
diff --git a/src/main/java/com/imyeyu/config/ConfigLoader.java b/src/main/java/com/imyeyu/config/ConfigLoader.java
new file mode 100644
index 0000000..5502245
--- /dev/null
+++ b/src/main/java/com/imyeyu/config/ConfigLoader.java
@@ -0,0 +1,237 @@
+package com.imyeyu.config;
+
+import com.imyeyu.io.IO;
+import com.imyeyu.java.TimiJava;
+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;
+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.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-10 00:44
+ */
+public class ConfigLoader {
+
+ private final File file;
+ private final Yaml yaml;
+ private final Class clazz;
+ private final List rawMappingList = new ArrayList<>();
+ private final Map, BaseConverter, ?>> converterMap = new HashMap<>();
+
+ private T value;
+ private LinkedHashMap rawValue;
+
+ public ConfigLoader(String srcPath, Class clazz) {
+ this(srcPath, srcPath, clazz);
+ }
+
+ public ConfigLoader(String srcPath, String distPath, Class 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()) {
+ try {
+ IO.resourceToDisk(getClass(), srcPath, distPath);
+ } catch (Exception e) {
+ throw new TimiException(TimiCode.ERROR, "load default config error", e);
+ }
+ }
+ }
+
+ @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 raw = rawValue;
+ String key;
+ if (bukkit.length == 1) {
+ key = bukkit[0];
+ } else {
+ for (int j = 0; j < bukkit.length - 1; j++) {
+ raw = (Map) 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>) type, item.getValue().toString()));
+ continue;
+ }
+ refField.getSetter().invoke(object, item.getValue());
+ }
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public Map getRawValue() {
+ return rawValue;
+ }
+
+ /**
+ * @author 夜雨
+ * @version 2024-04-10 00:45
+ */
+ private static class RawMapper {
+
+ Object owner;
+
+ RefField refField;
+
+ String rawStack;
+ }
+}
diff --git a/src/main/java/com/imyeyu/config/ConfigRepresenter.java b/src/main/java/com/imyeyu/config/ConfigRepresenter.java
new file mode 100644
index 0000000..acd5822
--- /dev/null
+++ b/src/main/java/com/imyeyu/config/ConfigRepresenter.java
@@ -0,0 +1,18 @@
+package com.imyeyu.config;
+
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.nodes.Tag;
+import org.yaml.snakeyaml.representer.Representer;
+
+/**
+ * @author 夜雨
+ * @version 2024-05-01 14:03
+ */
+public class ConfigRepresenter extends Representer {
+
+ public ConfigRepresenter(DumperOptions options) {
+ super(options);
+
+ this.nullRepresenter = data -> representScalar(Tag.NULL, "");
+ }
+}
diff --git a/src/main/java/com/imyeyu/config/Converter.java b/src/main/java/com/imyeyu/config/Converter.java
new file mode 100644
index 0000000..8b801f5
--- /dev/null
+++ b/src/main/java/com/imyeyu/config/Converter.java
@@ -0,0 +1,14 @@
+package com.imyeyu.config;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-26 00:30
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Converter {
+
+ Class extends BaseConverter, ?>> value();
+}
diff --git a/src/main/java/com/imyeyu/config/StringConverter.java b/src/main/java/com/imyeyu/config/StringConverter.java
new file mode 100644
index 0000000..89c8010
--- /dev/null
+++ b/src/main/java/com/imyeyu/config/StringConverter.java
@@ -0,0 +1,15 @@
+package com.imyeyu.config;
+
+import java.lang.reflect.Field;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-27 10:38
+ */
+public abstract class StringConverter extends BaseConverter {
+
+ @Override
+ protected String serialize(Field field, T t) {
+ return t.toString();
+ }
+}
diff --git a/src/test/java/test/Config.java b/src/test/java/test/Config.java
new file mode 100644
index 0000000..be806cd
--- /dev/null
+++ b/src/test/java/test/Config.java
@@ -0,0 +1,42 @@
+package test;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-25 15:37
+ */
+@Data
+public class Config {
+
+ public enum EnumTest {
+
+ TEST1,
+
+ TEST2
+ }
+
+ private String str;
+
+ private int num;
+
+ private Obj obj;
+
+ private boolean bool;
+
+ private EnumTest enumTest;
+
+ private ConfigConverterTest converterTest;
+
+ private int[] list;
+
+ @Data
+ public static class Obj {
+
+ private String objStr;
+
+ private List objList;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/test/ConfigConverterTest.java b/src/test/java/test/ConfigConverterTest.java
new file mode 100644
index 0000000..1bf3cac
--- /dev/null
+++ b/src/test/java/test/ConfigConverterTest.java
@@ -0,0 +1,13 @@
+package test;
+
+import lombok.Data;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-26 18:08
+ */
+@Data
+public class ConfigConverterTest {
+
+ private String value;
+}
diff --git a/src/test/java/test/TestConfig.java b/src/test/java/test/TestConfig.java
new file mode 100644
index 0000000..0f468db
--- /dev/null
+++ b/src/test/java/test/TestConfig.java
@@ -0,0 +1,106 @@
+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;
+
+/**
+ * @author 夜雨
+ * @version 2024-04-25 11:13
+ */
+public class TestConfig {
+
+ private static final String FILE_PATH = "test.yaml";
+
+ public ConfigLoader load(Class clazz) {
+ ConfigLoader loader = new ConfigLoader<>(FILE_PATH, clazz);
+ loader.load();
+ return loader;
+ }
+
+ public ConfigLoader reload(Class clazz) {
+ IO.destroy(new File(FILE_PATH));
+ return load(clazz);
+ }
+
+ @Test
+ public void testLoad() {
+ ConfigLoader