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..7d3b0cf
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,4 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+/CopilotChatHistory.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..bada8b5
--- /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..35eb1dd
--- /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..e7c1e9f
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ com.imyeyu.java
+ timi-java
+ 0.0.1
+ jar
+
+
+ 21
+ 21
+ UTF-8
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.3
+ test
+
+
+
diff --git a/src/main/java/com/imyeyu/java/TimiJava.java b/src/main/java/com/imyeyu/java/TimiJava.java
new file mode 100644
index 0000000..233e87d
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/TimiJava.java
@@ -0,0 +1,86 @@
+package com.imyeyu.java;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ *
+ * @author 夜雨
+ * @since 2021-02-13 11:39
+ */
+public interface TimiJava {
+
+ /**
+ * 通用判空
+ *
+ * TimiJava.isEmpty(null) = true;
+ * TimiJava.isEmpty("") = true;
+ * TimiJava.isEmpty(" ") = true;
+ * TimiJava.isEmpty((StringBuilder) "") = true;
+ * TimiJava.isEmpty((StringBuilder) " ") = true;
+ * TimiJava.isEmpty((StringBuffer) "") = true;
+ * TimiJava.isEmpty((StringBuffer) " ") = true;
+ * TimiJava.isEmpty([]) = true;
+ * TimiJava.isEmpty(list.size == 0) = true;
+ * TimiJava.isEmpty(set.size == 0) = true;
+ * TimiJava.isEmpty(map.size == 0) = true;
+ *
+ * otherwise = false;
+ *
+ *
+ * @param object 对象
+ * @return true 为空
+ */
+ static boolean isEmpty(Object object) {
+ if (object == null) {
+ return true;
+ }
+ if (object instanceof CharSequence charSequence) {
+ if (charSequence instanceof String string) {
+ return string.isEmpty() || string.trim().isEmpty();
+ } else {
+ return charSequence.toString().trim().isEmpty();
+ }
+ }
+ if (object.getClass().isArray()) {
+ return ((Object[]) object).length == 0;
+ }
+ if (object instanceof Collection> list) {
+ return list.isEmpty();
+ }
+ if (object instanceof Map, ?> map) {
+ return map.isEmpty();
+ }
+ return false;
+ }
+
+ /**
+ * {@link #isEmpty(Object)} 取反
+ *
+ * @param object 判定对象
+ * @return true 为非空
+ */
+ static boolean isNotEmpty(Object object) {
+ return !isEmpty(object);
+ }
+
+ @SafeVarargs
+ static T firstNotNull(T... objects) {
+ for (int i = 0; i < objects.length; i++) {
+ if (objects[i] != null) {
+ return objects[i];
+ }
+ }
+ return null;
+ }
+
+ @SafeVarargs
+ static T firstNotEmpty(T... objects) {
+ for (int i = 0; i < objects.length; i++) {
+ if (TimiJava.isNotEmpty(objects[i])) {
+ return objects[i];
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/Callback.java b/src/main/java/com/imyeyu/java/bean/Callback.java
new file mode 100644
index 0000000..83f041e
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/Callback.java
@@ -0,0 +1,20 @@
+package com.imyeyu.java.bean;
+
+/**
+ * 通用执行接口
+ *
+ * @author 夜雨
+ * @since 2022-02-09 20:06
+ */
+public interface Callback {
+
+ /** 执行程序 */
+ void handler() throws RuntimeException;
+
+ static void handle(Callback callback) {
+ if (callback == null) {
+ return;
+ }
+ callback.handler();
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/CallbackArg.java b/src/main/java/com/imyeyu/java/bean/CallbackArg.java
new file mode 100644
index 0000000..7fcf092
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/CallbackArg.java
@@ -0,0 +1,32 @@
+package com.imyeyu.java.bean;
+
+/**
+ * 通用回调接口
+ *
+ * @author 夜雨
+ * @since 2021-11-09 20:53
+ *
+ * @param 执行程序入参泛型
+ */
+public interface CallbackArg {
+
+ /**
+ * 执行程序
+ *
+ * @param t 入参
+ * @throws RuntimeException 运行时异常
+ */
+ void handler(T t) throws RuntimeException;
+
+ /**
+ * 非空时执行回调
+ *
+ * @param callbackArg 回调
+ */
+ static void handle(T t, CallbackArg callbackArg) {
+ if (callbackArg == null) {
+ return;
+ }
+ callbackArg.handler(t);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/CallbackArgReturn.java b/src/main/java/com/imyeyu/java/bean/CallbackArgReturn.java
new file mode 100644
index 0000000..3881b9b
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/CallbackArgReturn.java
@@ -0,0 +1,34 @@
+package com.imyeyu.java.bean;
+
+/**
+ * 通用可返回回调接口
+ *
+ * @author 夜雨
+ * @since 2022-05-26 14:24
+ *
+ * @param 执行程序入参泛型
+ * @param 执行程序返回泛型
+ */
+public interface CallbackArgReturn {
+
+ /**
+ * 执行程序
+ *
+ * @param t 入参
+ * @return 执行返回
+ * @throws RuntimeException 运行时异常
+ */
+ R handler(T t) throws RuntimeException;
+
+ /**
+ * 非空时执行回调
+ *
+ * @param callbackArgReturn 回调
+ */
+ static R handle(T t, CallbackArgReturn callbackArgReturn) {
+ if (callbackArgReturn == null) {
+ return null;
+ }
+ return callbackArgReturn.handler(t);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/CallbackReturn.java b/src/main/java/com/imyeyu/java/bean/CallbackReturn.java
new file mode 100644
index 0000000..d36a566
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/CallbackReturn.java
@@ -0,0 +1,32 @@
+package com.imyeyu.java.bean;
+
+/**
+ * 通用执行回调接口
+ *
+ * @author 夜雨
+ * @since 2022-02-09 19:59
+ *
+ * @param 执行返回泛型
+ */
+public interface CallbackReturn {
+
+ /**s
+ * 执行程序
+ *
+ * @return 执行返回
+ * @throws RuntimeException 运行时异常
+ */
+ R handler() throws RuntimeException;
+
+ /**
+ * 非空时执行回调
+ *
+ * @param callbackReturn 回调
+ */
+ static R handle(CallbackReturn callbackReturn) {
+ if (callbackReturn == null) {
+ return null;
+ }
+ return callbackReturn.handler();
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/Language.java b/src/main/java/com/imyeyu/java/bean/Language.java
new file mode 100644
index 0000000..c2cd28d
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/Language.java
@@ -0,0 +1,47 @@
+package com.imyeyu.java.bean;
+
+/**
+ * 多语言
+ *
+ * @author 夜雨
+ * @since 2022-02-23 11:25
+ */
+public enum Language {
+
+ /** 英语 */
+ en_US("English"),
+
+ /** 简中 */
+ zh_CN("简体中文"),
+
+ /** 繁中 */
+ zh_TW("繁体中文"),
+
+ /** 日语 */
+ ja_JP("日本語"),
+
+ /** 韩语 */
+ ko_KR("한국인"),
+
+ /** 俄语 */
+ ru_RU("русский"),
+
+ /** 德语 */
+ de_DE("Deutsch");
+
+ /** 名称 */
+ final String name;
+
+ Language(String name) {
+ this.name = name;
+ }
+
+ /**
+ * 获取语言名称
+ *
+ * @return 语言名称
+ */
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/LanguageMapping.java b/src/main/java/com/imyeyu/java/bean/LanguageMapping.java
new file mode 100644
index 0000000..7949abe
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/LanguageMapping.java
@@ -0,0 +1,18 @@
+package com.imyeyu.java.bean;
+
+/**
+ * @author 夜雨
+ * @since 2024-04-03 11:27
+ */
+public interface LanguageMapping {
+
+ void add(String key, String value);
+
+ boolean has(String key);
+
+ String text(String key);
+
+ String text(String key, String def);
+
+ String textArgs(String key, Object... args);
+}
diff --git a/src/main/java/com/imyeyu/java/bean/LanguageMsgMapping.java b/src/main/java/com/imyeyu/java/bean/LanguageMsgMapping.java
new file mode 100644
index 0000000..64ba65f
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/LanguageMsgMapping.java
@@ -0,0 +1,20 @@
+package com.imyeyu.java.bean;
+
+/**
+ * @author 夜雨
+ * @since 2024-04-01 10:28
+ */
+public interface LanguageMsgMapping {
+
+ T msg(String msg);
+
+ T msgKey(String msgKey);
+
+ void setMsg(String msg);
+
+ String getMsg();
+
+ void setMsgKey(String msgKey);
+
+ String getMsgKey();
+}
diff --git a/src/main/java/com/imyeyu/java/bean/package-info.java b/src/main/java/com/imyeyu/java/bean/package-info.java
new file mode 100644
index 0000000..ec58c6f
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/package-info.java
@@ -0,0 +1,2 @@
+/** 通用 Java Bean,通用接口 */
+package com.imyeyu.java.bean;
diff --git a/src/main/java/com/imyeyu/java/bean/timi/TimiCode.java b/src/main/java/com/imyeyu/java/bean/timi/TimiCode.java
new file mode 100644
index 0000000..d2fed91
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/timi/TimiCode.java
@@ -0,0 +1,135 @@
+package com.imyeyu.java.bean.timi;
+
+/**
+ * 通用代码(基于 HTTP 代码扩展)
+ *
+ * @author 夜雨
+ * @since 2021-05-21 14:32
+ */
+public enum TimiCode {
+
+ // ---------- 200 正常 ----------
+
+ /** 执行成功 */
+ SUCCESS(20000),
+
+ /** 执行成功,忽略请求 */
+ IGNORE (20001),
+
+ // ---------- 400 参数异常 ----------
+
+ /** 缺少参数 */
+ ARG_MISS(40000),
+
+ /** 不合法的参数 */
+ ARG_BAD(40001),
+
+ /** 过期的参数 */
+ ARG_EXPIRED(40002),
+
+ // ---------- 401 权限异常 ----------
+
+ /** 无权限 */
+ PERMISSION_MISS(40100),
+
+ /** 无权限 */
+ PERMISSION_ERROR (40101),
+
+ /** 非法请求 */
+ REQUEST_BAD(40102),
+
+ // ---------- 403 数据异常 ----------
+
+ /** 数据已存在 */
+ DATA_EXIST (40301),
+
+ // ---------- 404 资源异常 ----------
+
+ /** 无数据 */
+ RESULT_NULL(40400),
+
+ /** 返回数据异常 */
+ RESULT_BAD (40401),
+
+ /** 禁用的数据 */
+ RESULT_BAN (40402),
+
+ /** 上游服务器连接超时 */
+ RESULT_TIMEOUT (40403),
+
+ // ---------- 500 致命异常 ----------
+
+ /** 服务异常 */
+ ERROR(50000),
+
+ /** 服务异常 */
+ ERROR_NOT_SUPPORT(50001),
+
+ /** 服务异常 */
+ ERROR_NPE_VARIABLE(50002),
+
+ /** 服务关闭 */
+ ERROR_SERVICE_OFF(50003),
+
+ /** 服务繁忙 */
+ ERROR_SERVICE_BUSY(50300);
+
+ final Integer value;
+
+ TimiCode(Integer value) {
+ this.value = value;
+ }
+
+ /**
+ * 转为通用异常
+ *
+ * @return 异常
+ */
+ public TimiException toException() {
+ return toException(toString());
+ }
+
+ /**
+ * 转为通用异常
+ *
+ * @param msg 异常消息
+ * @return 异常
+ */
+ public TimiException toException(String msg) {
+ return new TimiException(this, msg);
+ }
+
+ /**
+ * 转为通用返回对象
+ *
+ * @return 返回对象
+ */
+ public TimiResponse> toResponse() {
+ return new TimiResponse<>(this).msg(toString());
+ }
+
+ /**
+ * 获取代码
+ *
+ * @return 代码
+ */
+ public Integer getValue() {
+ return value;
+ }
+
+ /**
+ * 根据代码返回对象
+ *
+ * @param code 代码
+ * @return 对象
+ */
+ public static TimiCode fromCode(int code) {
+ TimiCode[] codes = values();
+ for (int i = 0; i < codes.length; i++) {
+ if (codes[i].getValue() == code) {
+ return codes[i];
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/timi/TimiError.java b/src/main/java/com/imyeyu/java/bean/timi/TimiError.java
new file mode 100644
index 0000000..85cfd4c
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/timi/TimiError.java
@@ -0,0 +1,77 @@
+package com.imyeyu.java.bean.timi;
+
+import com.imyeyu.java.bean.LanguageMsgMapping;
+
+/**
+ * 致命错误
+ *
+ * @author 夜雨
+ * @since 2023-04-27 15:55
+ */
+public class TimiError extends AssertionError implements LanguageMsgMapping {
+
+ /** 代码 */
+ protected final TimiCode code;
+
+ protected String msgKey;
+
+ /** @param code 代码 */
+ public TimiError(TimiCode code) {
+ this.code = code;
+ }
+
+ /**
+ * 构造器
+ *
+ * @param code 代码
+ * @param msg 消息
+ */
+ public TimiError(TimiCode code, String msg) {
+ super(msg);
+ this.code = code;
+ }
+
+ /**
+ * 获取代码
+ *
+ * @return 代码
+ */
+ public TimiCode getCode() {
+ return code;
+ }
+
+ public TimiResponse> toResponse() {
+ return new TimiResponse<>(code).msg(getMessage()).msgKey(msgKey);
+ }
+
+ @Override
+ public TimiError msg(String msg) {
+ throw new UnsupportedOperationException("unsupported to change message");
+ }
+
+ @Override
+ public TimiError msgKey(String msgKey) {
+ setMsgKey(msgKey);
+ return this;
+ }
+
+ @Override
+ public void setMsg(String msg) {
+ throw new UnsupportedOperationException("unsupported to change message");
+ }
+
+ @Override
+ public String getMsg() {
+ return getMessage();
+ }
+
+ @Override
+ public void setMsgKey(String msgKey) {
+ this.msgKey = msgKey;
+ }
+
+ @Override
+ public String getMsgKey() {
+ return msgKey;
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/timi/TimiException.java b/src/main/java/com/imyeyu/java/bean/timi/TimiException.java
new file mode 100644
index 0000000..d76ef6f
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/timi/TimiException.java
@@ -0,0 +1,133 @@
+package com.imyeyu.java.bean.timi;
+
+import com.imyeyu.java.TimiJava;
+import com.imyeyu.java.bean.CallbackReturn;
+import com.imyeyu.java.bean.LanguageMsgMapping;
+
+/**
+ * 通用运行时异常,附加通用代码
+ *
+ * @author 夜雨
+ * @since 2021-05-20 15:18
+ */
+public class TimiException extends RuntimeException implements LanguageMsgMapping {
+
+ /** 代码 */
+ protected final TimiCode code;
+
+ protected String msgKey;
+
+ /** @param code 代码 */
+ public TimiException(TimiCode code) {
+ this.code = code;
+ }
+
+ /**
+ * 构造器
+ *
+ * @param code 代码
+ * @param msg 消息
+ */
+ public TimiException(TimiCode code, String msg) {
+ super(msg);
+ this.code = code;
+ }
+
+ /**
+ * 构造器
+ *
+ * @param code 代码
+ * @param msg 消息
+ */
+ public TimiException(TimiCode code, String msg, Throwable e) {
+ super(msg, e);
+ this.code = code;
+ }
+
+ /**
+ * 获取代码
+ *
+ * @return 代码
+ */
+ public TimiCode getCode() {
+ return code;
+ }
+
+ public TimiResponse> toResponse() {
+ return new TimiResponse<>(code).msg(getMessage()).msgKey(msgKey);
+ }
+
+ @Override
+ public TimiException msg(String msg) {
+ throw new UnsupportedOperationException("unsupported to change message");
+ }
+
+ @Override
+ public TimiException msgKey(String msgKey) {
+ setMsgKey(msgKey);
+ return this;
+ }
+
+ @Override
+ public void setMsg(String msg) {
+ throw new UnsupportedOperationException("unsupported to change message");
+ }
+
+ @Override
+ public String getMsg() {
+ return getMessage();
+ }
+
+ @Override
+ public void setMsgKey(String msgKey) {
+ this.msgKey = msgKey;
+ }
+
+ @Override
+ public String getMsgKey() {
+ return msgKey;
+ }
+
+ public static T required(T t, String message) {
+ if (TimiJava.isEmpty(t)) {
+ throw new TimiException(TimiCode.ARG_MISS, message);
+ }
+ return t;
+ }
+
+ public static void requiredNull(T t, String message) throws TimiException {
+ if (t != null) {
+ throw new TimiException(TimiCode.ERROR, message);
+ }
+ }
+
+ public static void requiredTrue(boolean bool, String message) throws TimiException {
+ if (bool) {
+ return;
+ }
+ throw new TimiException(TimiCode.ERROR, message);
+ }
+
+ public static void requiredFalse(boolean bool, String message) throws TimiException {
+ if (!bool) {
+ return;
+ }
+ throw new TimiException(TimiCode.ERROR, message);
+ }
+
+ public static void requiredTrue(CallbackReturn callback, String message) throws TimiException {
+ TimiException.required(callback, "not found callback");
+ if (callback.handler()) {
+ return;
+ }
+ throw new TimiException(TimiCode.ERROR, message);
+ }
+
+ public static void requiredFalse(CallbackReturn callback, String message) throws TimiException {
+ TimiException.required(callback, "not found callback");
+ if (!callback.handler()) {
+ return;
+ }
+ throw new TimiException(TimiCode.ERROR, message);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/timi/TimiResponse.java b/src/main/java/com/imyeyu/java/bean/timi/TimiResponse.java
new file mode 100644
index 0000000..c3b1e3c
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/timi/TimiResponse.java
@@ -0,0 +1,168 @@
+package com.imyeyu.java.bean.timi;
+
+import com.imyeyu.java.bean.LanguageMsgMapping;
+
+import java.io.Serializable;
+
+/**
+ * 通用接口返回对象
+ *
+ * @author 夜雨
+ * @since 2021-07-01 20:18
+ */
+public class TimiResponse implements Serializable, LanguageMsgMapping> {
+
+ /** 代码 */
+ protected Integer code;
+
+ /** 消息 */
+ protected String msg;
+
+ protected String msgKey;
+
+ /** 数据体 */
+ protected T data;
+
+ /** 默认构造器 */
+ public TimiResponse() {
+ }
+
+ /**
+ * 构造器
+ *
+ * @param code 代码
+ */
+ public TimiResponse(TimiCode code) {
+ this(code, null);
+ }
+
+ /**
+ * 构造器
+ *
+ * @param code 代码
+ * @param data 数据体
+ */
+ public TimiResponse(TimiCode code, T data) {
+ this(code, data, null);
+ }
+
+ /**
+ * 构造器
+ *
+ * @param code 代码
+ * @param data 数据体
+ * @param msg 消息
+ */
+ public TimiResponse(TimiCode code, T data, String msg) {
+ this.code = code.getValue();
+ this.data = data;
+ this.msg = msg;
+ }
+
+ /** @return true 为成功,code 为 20000 段 */
+ public boolean isSuccess() {
+ return code != null && code <= 30000;
+ }
+
+ /** @return true 为失败,isSuccess 取反 */
+ public boolean isFail() {
+ return !isSuccess();
+ }
+
+ /**
+ * 转为通用异常
+ *
+ * @return 异常
+ */
+ public TimiException toException() {
+ TimiCode code = TimiCode.fromCode(this.code);
+ if (code == null) {
+ throw new NullPointerException("unknow timi code: " + this.code);
+ }
+ return code.toException(msg);
+ }
+
+ /**
+ * 获取代码
+ *
+ * @return 代码
+ */
+ public Integer getCode() {
+ return code;
+ }
+
+ /**
+ * 设置代码
+ *
+ * @param code 代码
+ */
+ public void setCode(Integer code) {
+ this.code = code;
+ }
+
+ /**
+ * 获取数据体
+ *
+ * @return 数据体
+ */
+ public T getData() {
+ return data;
+ }
+
+ /**
+ * 设置数据体
+ *
+ * @param data 数据体
+ */
+ public void setData(T data) {
+ this.data = data;
+ }
+
+ /**
+ * 追加消息(避免和泛型字符串冲突)
+ *
+ * @param msg 消息
+ * @return 本对象
+ */
+ @Override
+ public TimiResponse msg(String msg) {
+ this.msg = msg;
+ return this;
+ }
+
+ /**
+ * 获取消息
+ *
+ * @return 消息
+ */
+ @Override
+ public String getMsg() {
+ return msg;
+ }
+
+ /**
+ * 设置消息
+ *
+ * @param msg 消息
+ */
+ @Override
+ public void setMsg(String msg) {
+ this.msg = msg;
+ }
+
+ @Override
+ public TimiResponse msgKey(String msgKey) {
+ this.msgKey = msgKey;
+ return this;
+ }
+
+ @Override
+ public String getMsgKey() {
+ return msgKey;
+ }
+
+ @Override
+ public void setMsgKey(String msgKey) {
+ this.msgKey = msgKey;
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/bean/timi/package-info.java b/src/main/java/com/imyeyu/java/bean/timi/package-info.java
new file mode 100644
index 0000000..fdb3713
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/bean/timi/package-info.java
@@ -0,0 +1,2 @@
+/** 个人常用的通用类 */
+package com.imyeyu.java.bean.timi;
\ No newline at end of file
diff --git a/src/main/java/com/imyeyu/java/obs/ChangeListener.java b/src/main/java/com/imyeyu/java/obs/ChangeListener.java
new file mode 100644
index 0000000..6caec26
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ChangeListener.java
@@ -0,0 +1,18 @@
+package com.imyeyu.java.obs;
+
+import java.util.List;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 15:15
+ */
+public interface ChangeListener {
+
+ void handler(T from, T to);
+
+ static void notifyListener(List> listenerList, T from, T to) {
+ for (int i = 0; i < listenerList.size(); i++) {
+ listenerList.get(i).handler(from, to);
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/CollectionChangeListener.java b/src/main/java/com/imyeyu/java/obs/CollectionChangeListener.java
new file mode 100644
index 0000000..c075b35
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/CollectionChangeListener.java
@@ -0,0 +1,29 @@
+package com.imyeyu.java.obs;
+
+import java.util.List;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 15:59
+ */
+public interface CollectionChangeListener {
+
+ /**
+ * @author 夜雨
+ * @since 2024-09-01 19:47
+ */
+ enum ChangeType {
+
+ ADD,
+
+ REMOVE
+ }
+
+ void handler(ChangeType type, E e) throws RuntimeException;
+
+ static void notifyListener(List> listenerList, ChangeType type, E element) {
+ for (int i = 0; i < listenerList.size(); i++) {
+ listenerList.get(i).handler(type, element);
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/MapChangeListener.java b/src/main/java/com/imyeyu/java/obs/MapChangeListener.java
new file mode 100644
index 0000000..b343b96
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/MapChangeListener.java
@@ -0,0 +1,34 @@
+package com.imyeyu.java.obs;
+
+import java.util.List;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 18:01
+ */
+public interface MapChangeListener {
+
+ /**
+ *
+ *
+ * @author 夜雨
+ * @since 2024-09-01 23:51
+ */
+ enum ChangeType {
+
+ ADD,
+
+ UPDATE,
+
+ REMOVE
+ }
+
+ void handler(ChangeType type, K key, V value);
+
+ @SuppressWarnings("unchecked")
+ static void notifyListener(List> listenerList, ChangeType type, Object key, V value) {
+ for (int i = 0; i < listenerList.size(); i++) {
+ listenerList.get(i).handler(type, (K) key, value);
+ }
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/Observable.java b/src/main/java/com/imyeyu/java/obs/Observable.java
new file mode 100644
index 0000000..07fa696
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/Observable.java
@@ -0,0 +1,16 @@
+package com.imyeyu.java.obs;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 15:07
+ */
+public interface Observable {
+
+ T get();
+
+ void set(T value);
+
+ void addListener(ChangeListener changeListener);
+
+ void removeListener(ChangeListener changeListener);
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableBoolean.java b/src/main/java/com/imyeyu/java/obs/ObservableBoolean.java
new file mode 100644
index 0000000..b65a8e2
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableBoolean.java
@@ -0,0 +1,16 @@
+package com.imyeyu.java.obs;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 18:43
+ */
+public class ObservableBoolean extends ObservableObject {
+
+ public ObservableBoolean() {
+ super();
+ }
+
+ public ObservableBoolean(Boolean value) {
+ super(value);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableDouble.java b/src/main/java/com/imyeyu/java/obs/ObservableDouble.java
new file mode 100644
index 0000000..316741b
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableDouble.java
@@ -0,0 +1,16 @@
+package com.imyeyu.java.obs;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 18:43
+ */
+public class ObservableDouble extends ObservableObject {
+
+ public ObservableDouble() {
+ super();
+ }
+
+ public ObservableDouble(Number value) {
+ super(value.doubleValue());
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableFloat.java b/src/main/java/com/imyeyu/java/obs/ObservableFloat.java
new file mode 100644
index 0000000..1a3c2ab
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableFloat.java
@@ -0,0 +1,16 @@
+package com.imyeyu.java.obs;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 18:43
+ */
+public class ObservableFloat extends ObservableObject {
+
+ public ObservableFloat() {
+ super();
+ }
+
+ public ObservableFloat(Float value) {
+ super(value);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableInteger.java b/src/main/java/com/imyeyu/java/obs/ObservableInteger.java
new file mode 100644
index 0000000..9047cc6
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableInteger.java
@@ -0,0 +1,15 @@
+package com.imyeyu.java.obs;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 15:06
+ */
+public class ObservableInteger extends ObservableObject {
+
+ public ObservableInteger() {
+ }
+
+ public ObservableInteger(Integer value) {
+ super(value);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableList.java b/src/main/java/com/imyeyu/java/obs/ObservableList.java
new file mode 100644
index 0000000..6dce667
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableList.java
@@ -0,0 +1,114 @@
+package com.imyeyu.java.obs;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 17:51
+ */
+public class ObservableList extends ArrayList {
+
+ private final List> changeListenerList = new ArrayList<>();
+
+ public ObservableList() {
+ }
+
+ public ObservableList(Collection extends E> c) {
+ super(c);
+ }
+
+ public void addChangeListener(CollectionChangeListener listener) {
+ changeListenerList.add(listener);
+ }
+
+ public void removeChangeListener(CollectionChangeListener listener) {
+ changeListenerList.add(listener);
+ }
+
+ @Override
+ public boolean add(E e) {
+ boolean result = super.add(e);
+ if (result) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.ADD, e);
+ }
+ return result;
+ }
+
+ @Override
+ public void add(int index, E element) {
+ super.add(index, element);
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.ADD, element);
+ }
+
+ @Override
+ public boolean addAll(Collection extends E> c) {
+ boolean result = super.addAll(c);
+ if (result) {
+ for (E e : c) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.ADD, e);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection extends E> c) {
+ boolean result = super.addAll(index, c);
+ if (result) {
+ for (E e : c) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.ADD, e);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public E remove(int index) {
+ E removed = super.remove(index);
+ if (removed != null) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.REMOVE, removed);
+ }
+ return removed;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ return remove(indexOf(o)) != null;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean removeAll(Collection> c) {
+ boolean result = super.removeAll(c);
+ if (result) {
+ for (Object e : c) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.REMOVE, (E) e);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean removeIf(Predicate super E> filter) {
+ boolean removed = false;
+ Iterator iterator = iterator();
+ while (iterator.hasNext()) {
+ E e = iterator.next();
+ if (filter.test(e)) {
+ iterator.remove(); // 此方法会触发 notifyListener
+ removed = true;
+ }
+ }
+ return removed;
+ }
+
+ @Override
+ public ObservableList subList(int fromIndex, int toIndex) {
+ List subList = super.subList(fromIndex, toIndex);
+ return new ObservableList<>(subList);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableLong.java b/src/main/java/com/imyeyu/java/obs/ObservableLong.java
new file mode 100644
index 0000000..985be58
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableLong.java
@@ -0,0 +1,16 @@
+package com.imyeyu.java.obs;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 18:43
+ */
+public class ObservableLong extends ObservableObject {
+
+ public ObservableLong() {
+ super();
+ }
+
+ public ObservableLong(Long value) {
+ super(value);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableMap.java b/src/main/java/com/imyeyu/java/obs/ObservableMap.java
new file mode 100644
index 0000000..c74167b
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableMap.java
@@ -0,0 +1,60 @@
+package com.imyeyu.java.obs;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 18:32
+ */
+public class ObservableMap extends HashMap {
+
+ private final List> changeListenerList = new ArrayList<>();
+
+ public void addChangeListener(MapChangeListener listener) {
+ changeListenerList.add(listener);
+ }
+
+ public void removeChangeListener(MapChangeListener listener) {
+ changeListenerList.add(listener);
+ }
+
+ @Override
+ public V put(K key, V value) {
+ V v = super.put(key, value);
+ if (v == null) {
+ MapChangeListener.notifyListener(changeListenerList, MapChangeListener.ChangeType.ADD, key, value);
+ } else if (!v.equals(value)) {
+ MapChangeListener.notifyListener(changeListenerList, MapChangeListener.ChangeType.UPDATE, key, value);
+ }
+ return v;
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ for (Map.Entry extends K, ? extends V> item : m.entrySet()) {
+ put(item.getKey(), item.getValue());
+ }
+ }
+
+ @Override
+ public V remove(Object key) {
+ V removed = super.remove(key);
+ if (removed != null) {
+ MapChangeListener.notifyListener(changeListenerList, MapChangeListener.ChangeType.REMOVE, key, removed);
+ }
+ return removed;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean remove(Object key, Object value) {
+ boolean result = super.remove(key, value);
+ if (result) {
+ MapChangeListener.notifyListener(changeListenerList, MapChangeListener.ChangeType.REMOVE, key, (V) value);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableObject.java b/src/main/java/com/imyeyu/java/obs/ObservableObject.java
new file mode 100644
index 0000000..c35f98b
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableObject.java
@@ -0,0 +1,47 @@
+package com.imyeyu.java.obs;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 15:22
+ */
+public abstract class ObservableObject implements Observable {
+
+ private final List> changeListenerList = new ArrayList<>();
+
+ private T value;
+
+ public ObservableObject() {
+ }
+
+ public ObservableObject(T value) {
+ this.value = value;
+ }
+
+ @Override
+ public final T get() {
+ return value;
+ }
+
+ @Override
+ public final void set(T toValue) {
+ final T from = this.value;
+ this.value = toValue;
+ boolean isChanged = !from.equals(toValue);
+ if (isChanged) {
+ ChangeListener.notifyListener(changeListenerList, from, toValue);
+ }
+ }
+
+ @Override
+ public void addListener(ChangeListener changeListener) {
+ changeListenerList.add(changeListener);
+ }
+
+ @Override
+ public void removeListener(ChangeListener changeListener) {
+ changeListenerList.remove(changeListener);
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableSet.java b/src/main/java/com/imyeyu/java/obs/ObservableSet.java
new file mode 100644
index 0000000..bf13e91
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableSet.java
@@ -0,0 +1,81 @@
+package com.imyeyu.java.obs;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 18:29
+ */
+public class ObservableSet extends HashSet {
+
+ private final List> changeListenerList = new ArrayList<>();
+
+ public void addChangeListener(CollectionChangeListener listener) {
+ changeListenerList.add(listener);
+ }
+
+ public void removeChangeListener(CollectionChangeListener listener) {
+ changeListenerList.add(listener);
+ }
+
+ @Override
+ public boolean add(E e) {
+ boolean result = super.add(e);
+ if (result) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.ADD, e);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean addAll(Collection extends E> c) {
+ boolean result = super.addAll(c);
+ if (result) {
+ for (E e : c) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.ADD, e);
+ }
+ }
+ return result;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean remove(Object o) {
+ boolean removed = super.remove(o);
+ if (removed) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.REMOVE, (E) o);
+ }
+ return removed;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean removeAll(Collection> c) {
+ boolean result = super.removeAll(c);
+ if (result) {
+ for (Object e : c) {
+ CollectionChangeListener.notifyListener(changeListenerList, CollectionChangeListener.ChangeType.REMOVE, (E) e);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean removeIf(Predicate super E> filter) {
+ boolean removed = false;
+ Iterator iterator = iterator();
+ while (iterator.hasNext()) {
+ E e = iterator.next();
+ if (filter.test(e)) {
+ iterator.remove(); // 此方法会触发 notifyListener
+ removed = true;
+ }
+ }
+ return removed;
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/obs/ObservableString.java b/src/main/java/com/imyeyu/java/obs/ObservableString.java
new file mode 100644
index 0000000..870f78f
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/obs/ObservableString.java
@@ -0,0 +1,29 @@
+package com.imyeyu.java.obs;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 18:43
+ */
+public class ObservableString extends ObservableObject {
+
+ public ObservableString() {
+ super();
+ }
+
+ public ObservableString(String value) {
+ super(value);
+ }
+
+ public boolean isEmpty() {
+ return get().isEmpty() || get().trim().isEmpty();
+ }
+
+ public boolean isNotEmpty() {
+ return !isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return get();
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/ref/Ref.java b/src/main/java/com/imyeyu/java/ref/Ref.java
new file mode 100644
index 0000000..59caf83
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/ref/Ref.java
@@ -0,0 +1,266 @@
+package com.imyeyu.java.ref;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 反射相关
+ *
+ * @author 夜雨
+ * @since 2023-05-04 15:05
+ */
+public class Ref {
+
+ /**
+ * 获取类字段列表
+ *
+ * @param clazz 类
+ * @return 字段列表
+ */
+ public static List listFields(Class> clazz) {
+ return List.of(clazz.getDeclaredFields());
+ }
+
+ /**
+ * 获取类字段列表(包括父类)
+ *
+ * @param clazz 类
+ * @return 字段列表
+ */
+ public static List listAllFields(Class> clazz) {
+ List result = new ArrayList<>();
+ while (clazz != null && clazz != Object.class) {
+ Collections.addAll(result, clazz.getDeclaredFields());
+ clazz = clazz.getSuperclass();
+ }
+ return result;
+ }
+
+ /**
+ *
+ * @param keyName
+ * @return
+ */
+ public static String getFieldName(String keyName) {
+ String[] splits = {"-", "_", " "};
+ StringBuilder full = new StringBuilder(keyName.substring(0, 1).toUpperCase() + keyName.substring(1));
+ for (int i = 0; i < splits.length; i++) {
+ if (keyName.contains(splits[i])) {
+ // 存在分隔符
+ full.setLength(0);
+ String[] word = keyName.split(splits[i]);
+ for (int j = 0; j < word.length; j++) {
+ full.append(word[j].substring(0, 1).toUpperCase()).append(word[j].substring(1));
+ }
+ break;
+ }
+ }
+ return String.valueOf(full.charAt(0)).toLowerCase() + full.substring(1);
+ }
+
+ /**
+ * 反射获取对象字段,包括父级类,直至 {@link Object},如果都不存在则返回 null
+ *
+ * @param clazz 类
+ * @param fieldName 字段名
+ * @return 字段
+ */
+ public static Field getField(Class> clazz, String fieldName) {
+ do {
+ try {
+ Field field = clazz.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field;
+ } catch (NoSuchFieldException e) {
+ clazz = clazz.getSuperclass();
+ }
+ } while (clazz != Object.class);
+ throw new NullPointerException(String.format("not found field: %s in %s", fieldName, clazz));
+ }
+
+ /**
+ * 反射获取对象字段值,包括父级类,直至 {@link Object}
+ *
+ * @param object 对象
+ * @param fieldName 字段名
+ * @param toClass 返回类
+ * @param 返回类型
+ * @return 字段值
+ * @throws IllegalAccessException 反射访问失败
+ * @throws NullPointerException 向上反射直至 {@link Object} 也找不到该字段
+ */
+ public static T getFieldValue(Object object, String fieldName, Class extends T> toClass) throws IllegalAccessException, NullPointerException {
+ return getFieldValue(object, getField(object.getClass(), fieldName), toClass);
+ }
+
+ /**
+ * 反射获取对象字段值
+ *
+ * @param object 对象
+ * @param field 字段
+ * @param toClass 返回类
+ * @param 返回类型
+ * @return 字段值
+ * @throws IllegalAccessException 反射访问失败
+ * @throws NullPointerException 向上反射直至 {@link Object} 也找不到该字段
+ */
+ public static T getFieldValue(Object object, Field field, Class extends T> toClass) throws IllegalAccessException, NullPointerException {
+ if (field == null) {
+ throw new NullPointerException("field can not be null");
+ }
+ field.setAccessible(true);
+ return toClass.cast(field.get(object));
+ }
+
+ /**
+ * 反射获取类字段
+ *
+ * @param objectClass 类
+ * @param fieldName 字段名
+ * @return 字段
+ */
+ public static Field getClassField(Class> objectClass, String fieldName) {
+ try {
+ Field field = objectClass.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field;
+ } catch (NoSuchFieldException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 反射获取指定类字段值
+ *
+ * @param object 对象
+ * @param objectClass 类
+ * @param fieldName 字段名
+ * @param toClass 值类型
+ * @param 值类型
+ * @return 字段值
+ * @throws IllegalAccessException 反射访问失败
+ * @throws NoSuchFieldException 字段不存在
+ */
+ public static T getClassFieldValue(Object object, Class> objectClass, String fieldName, Class toClass) throws IllegalAccessException, NoSuchFieldException {
+ Field field = getClassField(objectClass, fieldName);
+ if (field == null) {
+ throw new NoSuchFieldException("not found " + fieldName + " field in " + objectClass.getSimpleName());
+ }
+ field.setAccessible(true);
+ return toClass.cast(field.get(object));
+ }
+
+ /**
+ * 反射设置对象字段值,包括父级类,直至 {@link Object}
+ *
+ * @param object 对象
+ * @param fieldName 字段名
+ * @param value 字段值
+ * @throws IllegalAccessException 反射访问失败
+ * @throws NoSuchFieldException 向上反射直至 {@link Object} 也找不到该字段
+ */
+ public static void setFieldValue(Object object, String fieldName, Object value) throws IllegalAccessException, NoSuchFieldException {
+ Field field = getField(object.getClass(), fieldName);
+ if (field == null) {
+ throw new NoSuchFieldException("not found " + fieldName + " field in " + object.getClass().getSimpleName());
+ }
+ setFieldValue(object, field, value);
+ }
+
+ public static void setFieldValue(Object object, Field field, Object value) throws IllegalAccessException {
+ field.setAccessible(true);
+ field.set(object, value);
+ }
+
+ /**
+ * 反射设置对象字段值
+ *
+ * @param object 对象
+ * @param objectClass 类
+ * @param fieldName 字段名
+ * @param value 字段值
+ * @throws IllegalAccessException 反射访问失败
+ * @throws NullPointerException 字段不存在
+ */
+ public static void setClassFieldValue(Object object, Class> objectClass, String fieldName, Object value) throws IllegalAccessException, NullPointerException {
+ Field field = getClassField(objectClass, fieldName);
+ if (field == null) {
+ throw new NullPointerException("not found " + fieldName + " field in " + objectClass.getSimpleName() + " class");
+ }
+ field.setAccessible(true);
+ field.set(object, value);
+ }
+
+ /**
+ * 反射查找方法,包括父级类,直至 {@link Object},如果都不存在则返回 null
+ *
+ * @param clazz 类
+ * @param methodName 方法名
+ * @param parameterTypes 可选参
+ * @return 方法对象
+ */
+ public static Method getMethod(Class> clazz, String methodName, Class>... parameterTypes) {
+ if (clazz == null) {
+ throw new NullPointerException("class can not be null");
+ }
+ do {
+ try {
+ Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
+ method.setAccessible(true);
+ return method;
+ } catch (NoSuchMethodException e) {
+ clazz = clazz.getSuperclass();
+ }
+ } while (clazz != Object.class);
+ return null;
+ }
+
+ /**
+ * 字符串转枚举
+ *
+ * @param clazz 枚举类
+ * @param string 字符串
+ * @param 泛型
+ * @return 泛型
+ */
+ public static > T toType(Class clazz, String string) {
+ if (string == null) {
+ return null;
+ }
+ T[] ts = clazz.getEnumConstants();
+ if (ts == null) {
+ throw new IllegalArgumentException(clazz.getName() + " is not an enum type");
+ }
+ for (int i = 0; i < ts.length; i++) {
+ if (ts[i].name().equalsIgnoreCase(string)) {
+ return ts[i];
+ }
+ }
+ return null;
+ }
+
+ /**
+ *
+ *
+ * @param owner
+ * @param keyName
+ * @return
+ */
+ public static RefField field(Class> owner, String keyName) {
+ return new RefField(owner, keyName);
+ }
+
+ /**
+ *
+ * @param type
+ * @return
+ * @param
+ * @throws Exception
+ */
+ public static T newInstance(Class type) throws Exception {
+ return type.getDeclaredConstructor().newInstance();
+ }
+}
diff --git a/src/main/java/com/imyeyu/java/ref/RefField.java b/src/main/java/com/imyeyu/java/ref/RefField.java
new file mode 100644
index 0000000..93aa3f6
--- /dev/null
+++ b/src/main/java/com/imyeyu/java/ref/RefField.java
@@ -0,0 +1,77 @@
+package com.imyeyu.java.ref;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ *
+ *
+ * @author 夜雨
+ * @since 2024-04-26 15:46
+ */
+public class RefField {
+
+ private Class> type;
+ private Field field;
+ private Method setter;
+ private Method getter;
+
+ RefField(Class> owner, String keyName) {
+ String fieldName = Ref.getFieldName(keyName);
+ String fieldNameUpper = String.valueOf(fieldName.charAt(0)).toUpperCase() + fieldName.substring(1);
+
+ field = Ref.getField(owner, fieldName);
+ type = field.getType();
+
+ String setterName;
+ String getterName;
+
+ if (boolean.class.equals(field.getType()) || Boolean.class.equals(field.getType())) {
+ if (fieldName.startsWith("is")) {
+ setterName = "set" + fieldName.substring(2);
+ getterName = fieldName;
+ } else {
+ setterName = "set" + fieldNameUpper;
+ getterName = "is" + fieldNameUpper;
+ }
+ } else {
+ setterName = "set" + fieldNameUpper;
+ getterName = "get" + fieldNameUpper;
+ }
+
+ setter = Ref.getMethod(owner, setterName, field.getType());
+ getter = Ref.getMethod(owner, getterName);
+ }
+
+ public Class> getType() {
+ return type;
+ }
+
+ public void setType(Class> type) {
+ this.type = type;
+ }
+
+ public Field getField() {
+ return field;
+ }
+
+ public void setField(Field field) {
+ this.field = field;
+ }
+
+ public Method getSetter() {
+ return setter;
+ }
+
+ public void setSetter(Method setter) {
+ this.setter = setter;
+ }
+
+ public Method getGetter() {
+ return getter;
+ }
+
+ public void setGetter(Method getter) {
+ this.getter = getter;
+ }
+}
diff --git a/src/test/java/test/TestObs.java b/src/test/java/test/TestObs.java
new file mode 100644
index 0000000..9c4a9f7
--- /dev/null
+++ b/src/test/java/test/TestObs.java
@@ -0,0 +1,60 @@
+package test;
+
+import com.imyeyu.java.obs.ObservableList;
+import com.imyeyu.java.obs.ObservableMap;
+import com.imyeyu.java.obs.ObservableString;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+/**
+ * @author 夜雨
+ * @since 2024-09-01 23:26
+ */
+public class TestObs {
+
+ @Test
+ public void testString() {
+ ObservableString obsString = new ObservableString("test1");
+ System.out.println("init value = " + obsString);
+ obsString.addListener((from, to) -> System.out.printf("from %s to %s%n", from, to));
+ obsString.set("test2");
+ System.out.println("now value = " + obsString);
+ assert obsString.get().equals("test2");
+ }
+
+ @Test
+ public void testList() {
+ ObservableList list = new ObservableList<>();
+ list.addChangeListener((type, string) -> System.out.printf("%s element %s to list%n", type, string));
+ list.add("test1");
+ list.add("test2");
+ list.add("test3");
+ list.add("test4");
+ list.add("test5");
+ list.addAll(List.of("test7", "test8"));
+ list.add(0, "test0");
+ System.out.println(list);
+ list.remove("test2");
+ list.remove(0);
+ list.removeAll(List.of("test3", "test4"));
+ list.removeIf(item -> item.equals("test1")); // 移除 0, 1, 2, 3, 4
+ System.out.println(list);
+ }
+
+ @Test
+ public void testMap() {
+ ObservableMap map = new ObservableMap<>();
+ map.addChangeListener((type, key, value) -> System.out.printf("%s element %s:%s to map%n", type, key, value));
+ map.put("t1", "test1");
+ map.put("t1", "test1update"); // test1 更新
+ map.put("t2", "test2");
+ map.put("t2", "test2"); // test2 不更新
+ map.put("t3", "test3");
+ map.put("t4", "test4");
+ map.put("t5", "test5");
+ map.remove("t1");
+ map.remove("t2", "test2");
+ System.out.println(map);
+ }
+}
diff --git a/src/test/java/test/TestRef.java b/src/test/java/test/TestRef.java
new file mode 100644
index 0000000..7328f4f
--- /dev/null
+++ b/src/test/java/test/TestRef.java
@@ -0,0 +1,96 @@
+package test;
+
+import com.imyeyu.java.ref.Ref;
+import com.imyeyu.java.ref.RefField;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author 夜雨
+ * @since 2024-04-26 11:05
+ */
+public class TestRef {
+
+ private boolean isBoy;
+ private boolean boy;
+ private String fieldName;
+
+ @Test
+ public void test() {
+ String t1 = Ref.getFieldName("is-boy");
+ assert t1.equals("isBoy");
+ String t2 = Ref.getFieldName("is_boy");
+ assert t2.equals("isBoy");
+ String t3 = Ref.getFieldName("is boy");
+ assert t3.equals("isBoy");
+ String t4 = Ref.getFieldName("isBoy");
+ assert t4.equals("isBoy");
+ String t5 = Ref.getFieldName("boy");
+ assert t5.equals("boy");
+ String t6 = Ref.getFieldName("fieldName");
+ assert t6.equals("fieldName");
+ String t7 = Ref.getFieldName("field name");
+ assert t7.equals("fieldName");
+ String t8 = Ref.getFieldName("field-Name");
+ assert t8.equals("fieldName");
+ String t9 = Ref.getFieldName("field_Name");
+ assert t9.equals("fieldName");
+ }
+
+ @Test
+ public void refBooleanField() {
+ RefField refField = Ref.field(TestRef.class, "is-boy");
+ assert refField.getSetter() != null && refField.getSetter().getName().equals("setBoy");
+ assert refField.getGetter() != null && refField.getGetter().getName().equals("isBoy");
+ }
+
+ @Test
+ public void refBoolean2Field() {
+ RefField refField = Ref.field(TestRef.class, "boy");
+ assert refField.getSetter() != null && refField.getSetter().getName().equals("setBoy");
+ assert refField.getGetter() != null && refField.getGetter().getName().equals("isBoy");
+ }
+
+ @Test
+ public void refStringField() {
+ RefField refField = Ref.field(TestRef.class, "fieldName");
+ assert refField.getSetter() != null && refField.getSetter().getName().equals("setFieldName");
+ assert refField.getGetter() != null && refField.getGetter().getName().equals("getFieldName");
+ }
+
+ @Test
+ public void refString2Field() {
+ RefField refField = Ref.field(TestRef.class, "field-name");
+ assert refField.getSetter() != null && refField.getSetter().getName().equals("setFieldName");
+ assert refField.getGetter() != null && refField.getGetter().getName().equals("getFieldName");
+ }
+
+ @Test
+ public void refString3Field() {
+ RefField refField = Ref.field(TestRef.class, "field_name");
+ assert refField.getSetter() != null && refField.getSetter().getName().equals("setFieldName");
+ assert refField.getGetter() != null && refField.getGetter().getName().equals("getFieldName");
+ }
+
+ @Test
+ public void refString4Field() {
+ RefField refField = Ref.field(TestRef.class, "field name");
+ assert refField.getSetter() != null && refField.getSetter().getName().equals("setFieldName");
+ assert refField.getGetter() != null && refField.getGetter().getName().equals("getFieldName");
+ }
+
+ public boolean isBoy() {
+ return isBoy;
+ }
+
+ public void setBoy(boolean boy) {
+ isBoy = boy;
+ }
+
+ public String getFieldName() {
+ return fieldName;
+ }
+
+ public void setFieldName(String fieldName) {
+ this.fieldName = fieldName;
+ }
+}
\ No newline at end of file