diff --git a/README.md b/README.md
index 929c64d2e843c9e79e7dc618780a76c7ff98f32d..bbd0d0a67f03ac12d8de61fb746d3a6df419aef3 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-able</artifactId>
-        <version>5.4.6</version>
+        <version>5.5.15</version>
     </dependency>
 ```
 ```xml
     <dependency>
         <groupId>com.gitee.qdbp</groupId>
         <artifactId>qdbp-tools</artifactId>
-        <version>5.4.6</version>
+        <version>5.5.15</version>
     </dependency>
 ```
\ No newline at end of file
diff --git a/able/pom.xml b/able/pom.xml
index 80d5e70d94fb018bc5bf3e6006a3af8ecbb61bce..c80e177a6152af89682d856a6ed99e0b9ca37694 100644
--- a/able/pom.xml
+++ b/able/pom.xml
@@ -5,11 +5,11 @@
 	<parent>	
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-parent</artifactId>
-		<version>5.0.1</version>
+		<version>5.0.2</version>
 	</parent>
 
 	<artifactId>qdbp-able</artifactId>
-	<version>5.4.6</version>
+	<version>5.5.15</version>
 	<packaging>jar</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/AcceptAttrs.java b/able/src/main/java/com/gitee/qdbp/able/beans/AcceptAttrs.java
index b1f90f388365a94e5f1bc96f1810538b4a4db98c..ba48ddd3b78e9083acb4c7e586362db14198db23 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/AcceptAttrs.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/AcceptAttrs.java
@@ -27,11 +27,28 @@ public class AcceptAttrs<T> implements Serializable {
     protected T defaultValue;
     /** 是否必填 **/
     protected boolean required;
+    /** 允许的数据类型 **/
+    protected final Class<T> acceptType;
     /** 允许的值 **/
     protected Collection<T> acceptValues;
 
+    /**
+     * 默认构造函数
+     * @deprecated 改为 AcceptAttrs(Class) 以便确定数据类型
+     */
+    @Deprecated
     public AcceptAttrs() {
-        acceptValues = new HashSet<>();
+        this(null);
+    }
+
+    public AcceptAttrs(Class<T> acceptType) {
+        this.acceptType = acceptType;
+        this.acceptValues = new HashSet<>();
+    }
+
+    /** 允许的数据类型 **/
+    public Class<T> getAcceptType() {
+        return acceptType;
     }
 
     /** 默认值 **/
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/DepthMap.java b/able/src/main/java/com/gitee/qdbp/able/beans/DepthMap.java
index b0f70d01b6a119a98f4f09158ee6c60c4443c7ff..42f9509356e5c917295f1fdec10826e90c772b4f 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/DepthMap.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/DepthMap.java
@@ -1,15 +1,12 @@
 package com.gitee.qdbp.able.beans;
 
 import java.io.Serializable;
-import java.util.Arrays;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
-import java.util.regex.Pattern;
-import com.gitee.qdbp.tools.utils.ReflectTools;
+import com.gitee.qdbp.tools.utils.MapTools;
 
 /**
  * 深度路径Map, 用于Ognl取值<br>
@@ -23,7 +20,7 @@ import com.gitee.qdbp.tools.utils.ReflectTools;
  *
  * dpm.put("code.folder.service", "service");
  * dpm.put("code.folder.page", "views");
- * dpm.put("code.folder", "java"); // 该设置无效, 会被忽略掉
+ * // dpm.put("code.folder", "java"); // 该设置会将上面两行覆盖掉
  *
  * Map<String, Object> map = dpm.map();
  *
@@ -39,13 +36,11 @@ import com.gitee.qdbp.tools.utils.ReflectTools;
  * @author zhaohuihua
  * @version 151221
  */
-public class DepthMap implements Serializable {
+public class DepthMap implements Copyable, Serializable {
 
     /** serialVersionUID **/
     private static final long serialVersionUID = 1L;
 
-    private static final Pattern SEPARATOR = Pattern.compile("\\.");
-
     private Map<String, Object> map = new HashMap<>();
 
     public DepthMap() {
@@ -57,46 +52,107 @@ public class DepthMap implements Serializable {
 
     @SuppressWarnings("unchecked")
     public DepthMap put(String key, Object value) {
-        String[] keys = SEPARATOR.split(key);
-
-        List<String> list = Arrays.asList(keys);
-        Iterator<String> iterator = list.iterator();
-
-        Map<String, Object> parent = map;
-        while (parent != null && iterator.hasNext()) {
-            String name = iterator.next();
-
-            Object older = parent.get(name);
-            if (iterator.hasNext()) {
-                // 有下一级
-                if (!(older instanceof Map)) {
-                    // !(older instanceof Map):
-                    // 如果之前有a.b.c, 新加一个a.b.c.d, 覆盖掉a.b.c
-                    older = new HashMap<>();
-                    parent.put(name, older);
-                }
-                parent = (Map<String, Object>) older;
-            } else {
-                // 没有下一级
-                if (older instanceof Map) {
-                    // 如果之前有a.b.c.d, 新加一个a.b.c, 忽略a.b.c
-                    continue;
-                }
-                parent.put(name, value);
-                parent = null; // 没有下一级了, 不需要再处理parent
-            }
-        }
+        MapTools.putValue(map, key, value);
         return this;
     }
 
+    @SuppressWarnings("unchecked")
     public <T> T get(String key) {
-        return ReflectTools.getDepthValue(this.map, key);
+        return (T) MapTools.getValue(map, key);
+    }
+
+    /**
+     * 判断Map中是否存在指定的KEY
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 是否存在
+     */
+    public boolean containsKey(String key) {
+        return MapTools.containsKey(map, key);
+    }
+
+    /**
+     * 获取字符串类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getString("xxx")将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 字符串
+     */
+    public String getString(String key) {
+        return MapTools.getString(map, key);
+    }
+
+    /**
+     * 获取Map值
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return Map值
+     */
+    public Object getValue(String key) {
+        return MapTools.getValue(map, key);
+    }
+
+    /**
+     * 获取指定类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param clazz 指定类型
+     * @return Map值
+     */
+    public <T> T getValue(String key, Class<T> clazz) {
+        return MapTools.getValue(map, key, clazz);
+    }
+
+    /**
+     * 获取指定类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), ConvertTools.getValue("xxx", Double.class)将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param defaults 值不存在或值类型不匹配时返回的默认值
+     * @param clazz 指定类型
+     * @return Map值
+     */
+    public <T> T getValue(String key, T defaults, Class<T> clazz) {
+        return MapTools.getValue(map, key, defaults, clazz);
+    }
+
+    /**
+     * 获取指定类型的Map值列表
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param clazz 指定类型
+     * @return Map值列表, 值不存在时将返回空List而不是null
+     */
+    public <T> List<T> getList(String key, Class<T> clazz) {
+        return MapTools.getList(map, key, clazz);
+    }
+
+    /**
+     * 获取子Map
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0]
+     * @return 子Map, 值不存在时将返回空Map而不是null
+     */
+    public Map<String, Object> getSubMap(String key) {
+        return MapTools.getSubMap(map, key);
+    }
+
+    /**
+     * 获取子Map数组
+     *
+     * @param key KEY, 支持多级, 如user.addresses
+     * @return 子Map数组, 值不存在时将返回空List而不是null
+     */
+    public List<Map<String, Object>> getSubMaps(String key) {
+        return MapTools.getSubMaps(map, key);
     }
 
     public Map<String, Object> map() {
         return map;
     }
 
+    @Override
     public DepthMap copy() {
         DepthMap n = new DepthMap();
         n.map.putAll(this.map);
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/KeyStrings.java b/able/src/main/java/com/gitee/qdbp/able/beans/KeyStrings.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fe08655d36f1cb4833fbde99c09ec515ab1c56e
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/KeyStrings.java
@@ -0,0 +1,22 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * KeyString类的数组
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class KeyStrings extends ArrayList<KeyString> {
+    private static final long serialVersionUID = -2726116922920104529L;
+
+    public Map<String, String> asMap() {
+        return KeyString.toMap(this);
+    }
+
+    public Map<String, Object> asJson() {
+        return KeyString.toMap(this);
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java b/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
index 0a62f92280a4b7a586bd90bc5dec258b30eba9f7..3a92aa6b737b0da7c34d701b1f2ec67708f52de6 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/KeyValue.java
@@ -2,9 +2,10 @@ package com.gitee.qdbp.able.beans;
 
 import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -59,6 +60,23 @@ public class KeyValue<V> implements Comparable<KeyValue<V>>, Serializable {
         this.value = value;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        KeyValue<?> keyValue = (KeyValue<?>) o;
+        return Objects.equals(key, keyValue.key) && Objects.equals(value, keyValue.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(key, value);
+    }
+
     /**
      * List转换为Map
      *
@@ -71,7 +89,7 @@ public class KeyValue<V> implements Comparable<KeyValue<V>>, Serializable {
             return null;
         }
 
-        Map<String, V> map = new HashMap<>();
+        Map<String, V> map = new LinkedHashMap<>();
         for (KeyValue<C> i : list) {
             map.put(i.key, i.value);
         }
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/LinkedListMap.java b/able/src/main/java/com/gitee/qdbp/able/beans/LinkedListMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..cc08ca39f50f013368147ebe9326913e3162cf1e
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/LinkedListMap.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.utils.MapTools;
+
+/**
+ * 用来装List对象的容器
+ *
+ * @author zhaohuihua
+ * @version 20230315
+ */
+public class LinkedListMap extends LinkedHashMap<String, Object> {
+
+    private static final long serialVersionUID = -1985897783368043967L;
+
+    public LinkedListMap(List<Map<String, Object>> list) {
+        for (int i = 0; i < list.size(); i++) {
+            this.put(String.valueOf(i), list.get(i));
+        }
+    }
+
+    public List<Map<String, Object>> content() {
+        return MapTools.toJsonMaps(this.values());
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/ModifyStatus.java b/able/src/main/java/com/gitee/qdbp/able/beans/ModifyStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..8199f218d5920a59b48fd7df9eb367dc861c69bd
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/ModifyStatus.java
@@ -0,0 +1,44 @@
+package com.gitee.qdbp.able.beans;
+
+/**
+ * 可修改状态
+ *
+ * @author zhaohuihua
+ * @version 20211219
+ */
+public class ModifyStatus {
+
+    /** 是否可修改 **/
+    private boolean modifiable = true;
+
+    /** 构造函数 **/
+    public ModifyStatus() {
+    }
+
+    /** 构造函数 **/
+    public ModifyStatus(boolean modifiable) {
+        this.modifiable = modifiable;
+    }
+
+    /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
+    public void lockToUnmodifiable() {
+        this.modifiable = false;
+    }
+
+    /** 设置是否可修改 **/
+    public void setModifiable(boolean modifiable) {
+        this.modifiable = modifiable;
+    }
+
+    /** 判断是否可修改 **/
+    public boolean isModifiable() {
+        return modifiable;
+    }
+
+    /** 检查是否可修改 **/
+    public void checkModifiable() {
+        if (!modifiable) {
+            throw new UnsupportedOperationException("unmodifiable");
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java b/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
index 50f8f89061a8039885e1943f461c7707d1e18981..1f0f5c1fc69b95dd7b8a65939bc38eaa4e4bbe9d 100644
--- a/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/ParamBuffer.java
@@ -1,6 +1,5 @@
 package com.gitee.qdbp.able.beans;
 
-import java.util.regex.Pattern;
 import com.gitee.qdbp.tools.utils.StringTools;
 
 /**
@@ -9,11 +8,7 @@ import com.gitee.qdbp.tools.utils.StringTools;
  * @author zhaohuihua
  * @version 180814
  */
-public class ParamBuffer implements Cloneable {
-
-    private static final Pattern TAB = Pattern.compile("\\t");
-    private static final Pattern RETURN = Pattern.compile("\\r");
-    private static final Pattern NEWLINE = Pattern.compile("\\n");
+public class ParamBuffer implements Copyable {
 
     private String separator;
     private int valueStringMaxLength;
@@ -58,23 +53,13 @@ public class ParamBuffer implements Cloneable {
         if (value == null) {
             return "";
         }
-        String string = value.toString();
-        // 替换掉\t\r\n
-        if (string.indexOf('\t') >= 0) {
-            string = TAB.matcher(string).replaceAll("\\t");
-        }
-        if (string.indexOf('\r') >= 0) {
-            string = RETURN.matcher(string).replaceAll("\\r");
-        }
-        if (string.indexOf('\n') >= 0) {
-            string = NEWLINE.matcher(string).replaceAll("\\n");
-        }
+        String string = StringTools.replace(value.toString(), "\t", "\\t", "\r", "\\r", "\n", "\\n");
         // 裁切过长的字符串
         return StringTools.ellipsis(string, valueStringMaxLength);
     }
 
     @Override
-    public ParamBuffer clone() {
+    public ParamBuffer copy() {
         ParamBuffer copy = new ParamBuffer();
         copy.separator = this.separator;
         copy.valueStringMaxLength = this.valueStringMaxLength;
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java b/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff36d1508f514209f022ff543ca98cfa01f4d62f
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/StringWithAttrs.java
@@ -0,0 +1,47 @@
+package com.gitee.qdbp.able.beans;
+
+import java.io.Serializable;
+
+/**
+ * 带属性的字符串
+ *
+ * @author zhaohuihua
+ * @version 20220804
+ */
+public class StringWithAttrs implements Serializable {
+
+    private static final long serialVersionUID = 3186043836380668366L;
+
+    /** 字符串内容 **/
+    private String string;
+    /** 属性列表 **/
+    private KeyStrings attrs;
+
+    public StringWithAttrs() {
+    }
+
+    public StringWithAttrs(String string, KeyStrings attrs) {
+        this.string = string;
+        this.attrs = attrs;
+    }
+
+    /** 字符串内容 **/
+    public String getString() {
+        return string;
+    }
+
+    /** 字符串内容 **/
+    public void setString(String string) {
+        this.string = string;
+    }
+
+    /** 属性列表 **/
+    public KeyStrings getAttrs() {
+        return attrs;
+    }
+
+    /** 属性列表 **/
+    public void setAttrs(KeyStrings attrs) {
+        this.attrs = attrs;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java b/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a79c519ed00b6eddc0465f9e8c77545e4193e8f
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/SubString.java
@@ -0,0 +1,83 @@
+package com.gitee.qdbp.able.beans;
+
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+import com.gitee.qdbp.tools.compare.CompareTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 子文本信息
+ *
+ * @author zhaohuihua
+ * @version 20210727
+ */
+public class SubString extends ExtraData implements Comparable<SubString> {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    /** 子文本内容 **/
+    private String content;
+    /** 子文本在原文本的开始位置 **/
+    private int startIndex;
+    /** 子文本在原文本的结束位置 **/
+    private int endIndex;
+
+    public SubString() {
+    }
+
+    public SubString(String content, int start, int end) {
+        this.content = content;
+        this.startIndex = start;
+        this.endIndex = end;
+    }
+
+    /** 子文本内容 **/
+    public String getContent() {
+        return content;
+    }
+
+    /** 子文本内容 **/
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    /** 子文本在原文本的开始位置 **/
+    public int getStartIndex() {
+        return startIndex;
+    }
+
+    /** 子文本在原文本的开始位置 **/
+    public void setStartIndex(int startIndex) {
+        this.startIndex = startIndex;
+    }
+
+    /** 子文本在原文本的结束位置 **/
+    public int getEndIndex() {
+        return endIndex;
+    }
+
+    /** 子文本在原文本的结束位置 **/
+    public void setEndIndex(int endIndex) {
+        this.endIndex = endIndex;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append(StringTools.pad(startIndex, 3));
+        buffer.append(':').append(StringTools.pad(endIndex, 3));
+        buffer.append("    ").append(content);
+        return buffer.toString();
+    }
+
+    @Override
+    public int compareTo(SubString o) {
+        if (o == null) {
+            return -1;
+        }
+        return CompareTools.stream()
+                .asc(startIndex, o.getStartIndex())
+                .asc(endIndex, o.getEndIndex())
+                .compare();
+    }
+}
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/SubStrings.java b/able/src/main/java/com/gitee/qdbp/able/beans/SubStrings.java
new file mode 100644
index 0000000000000000000000000000000000000000..f1bc64561b90c5c0af37805803cdbcd00c12f2f0
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/SubStrings.java
@@ -0,0 +1,71 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.ArrayList;
+import java.util.Map;
+import com.gitee.qdbp.able.model.reusable.ExtraBase;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+
+/**
+ * 子文本信息集合
+ *
+ * @author zhaohuihua
+ * @version 20220130
+ */
+public class SubStrings extends ArrayList<SubString> implements ExtraBase {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    private ExtraData extraData;
+
+    /** 获取附加数据 **/
+    @Override
+    public Map<String, Object> getExtra() {
+        return extraData == null ? null : extraData.getExtra();
+    }
+
+    /** 设置附加数据 **/
+    @Override
+    public void setExtra(Map<String, Object> extra) {
+        this.extraData = new ExtraData(extra);
+    }
+
+    /** 是否存在指定的附加数据 **/
+    @Override
+    public boolean containsExtra(String key) {
+        return extraData != null && extraData.containsExtra(key);
+    }
+
+    /** 获取指定的附加数据 **/
+    @Override
+    public Object getExtra(String key) {
+        return extraData == null ? null : extraData.getExtra(key);
+    }
+
+    /** 获取指定类型的附加数据 **/
+    @Override
+    public <T> T getExtra(String key, Class<T> clazz) {
+        return extraData == null ? null : extraData.getExtra(key, clazz);
+    }
+
+    /** 设置指定的附加数据 **/
+    @Override
+    public void putExtra(String key, Object value) {
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(key, value);
+    }
+
+    /** 设置附加信息 **/
+    @Override
+    public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(extra);
+    }
+}
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
new file mode 100644
index 0000000000000000000000000000000000000000..725272280a3309131c7b3d95d727a063e7400fa5
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/beans/TreeNodes.java
@@ -0,0 +1,475 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import com.gitee.qdbp.able.function.BaseFunction;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * 树形结构通用处理容器
+ *
+ * @author zhaohuihua
+ * @version 20220813
+ */
+public class TreeNodes<E> {
+
+    /** 容器节点 **/
+    private final Node<E> container;
+    /** KEY与节点的映射表 **/
+    private final Map<String, Node<E>> keyNodeMaps;
+    /** 未找到上级的KEY **/
+    private final List<String> keysOfWithoutParent;
+    /** 获取KEY的方法 **/
+    private final BaseFunction<E, String> keyGetter;
+
+    public TreeNodes(List<E> list, BaseFunction<E, String> keyGetter, BaseFunction<E, String> parentGetter) {
+        this(list, false, keyGetter, parentGetter);
+    }
+
+    public TreeNodes(List<E> list, boolean upgrade, BaseFunction<E, String> keyGetter, BaseFunction<E, String> parentGetter) {
+        Node<E> container = new Node<>(null);
+        String rootCode = "0";
+        List<Node<E>> all = new ArrayList<>();
+        // key - Node
+        Map<String, Node<E>> maps = new ConcurrentHashMap<>();
+        maps.put(rootCode, container); // 根节点
+
+        for (E element : list) {
+            Node<E> item = new Node<>(element);
+            all.add(item);
+            String key = keyGetter.apply(element);
+            maps.put(key, item);
+        }
+
+        // 未找到上级的KEY
+        List<String> keysOfWithoutParent = new ArrayList<>();
+        for (Node<E> item : all) {
+            String parentKey = parentGetter.apply(item.element);
+            if (VerifyTools.isBlank(parentKey)) {
+                maps.get(rootCode).addChildNode(item);
+            } else if (maps.containsKey(parentKey)) {
+                maps.get(parentKey).addChildNode(item);
+            } else {
+                String key = keyGetter.apply(item.element);
+                keysOfWithoutParent.add(key);
+                if (upgrade) {
+                    container.addChildNode(item);
+                }
+            }
+        }
+        this.container = container;
+        this.keyNodeMaps = maps;
+        this.keysOfWithoutParent = keysOfWithoutParent;
+        this.keyGetter = keyGetter;
+    }
+
+    /** 获取容器节点 **/
+    public Node<E> getContainer() {
+        return this.container;
+    }
+
+    /** 获取所有根级元素 **/
+    public List<E> getRootElements() {
+        return this.container.getChildElements();
+    }
+
+    /** 获取所有叶子元素 **/
+    public List<E> getAllLeafElements() {
+        return container.getAllLeafElements();
+    }
+
+    /** 获取所有后代元素 **/
+    public List<E> getAllDescendantElements() {
+        return container.getAllDescendantElements();
+    }
+
+    /** 查找指定元素 **/
+    public E findElement(String key) {
+        Node<E> item = findNode(key);
+        return item == null ? null : item.element;
+    }
+
+    /** 查找指定节点 **/
+    public Node<E> findNode(String key) {
+        Node<E> node = keyNodeMaps.get(key);
+        if (node != null) {
+            // 考虑到有可能在外部修改key, 因此增加检查
+            String nodeKey = keyGetter.apply(node.element);
+            if (key.equals(nodeKey)) {
+                return node;
+            }
+        }
+        Iterator<Node<E>> iterator = container.depthFirstNodeIterator();
+        while (iterator.hasNext()) {
+            Node<E> next = iterator.next();
+            String nextKey = keyGetter.apply(next.element);
+            // 遍历时更新缓存中的对应关系
+            keyNodeMaps.put(nextKey, next);
+            if (key.equals(nextKey)) {
+                return next;
+            }
+        }
+        return null;
+    }
+
+    /** 获取KEY对应元素的父元素 **/
+    public E findParentElement(String key) {
+        Node<E> item = findNode(key);
+        return item == null || item.parent == null ? null : item.parent.element;
+    }
+
+    /** 获取KEY对应元素的所有祖先元素 **/
+    public List<E> findAllAncestorElements(String key) {
+        return findAllAncestorElements(key, false);
+    }
+
+    /** 获取KEY对应元素的所有祖先元素, 可以指定是否包含指定KEY元素自身 **/
+    public List<E> findAllAncestorElements(String key, boolean includeSelf) {
+        Node<E> item = findNode(key);
+        if (item == null) {
+            return new ArrayList<>();
+        }
+        List<E> parents = item.getAllAncestorElements();
+        if (includeSelf) {
+            parents.add(item.element);
+        }
+        return parents;
+    }
+
+    /** 获取KEY对应元素的直接子元素 **/
+    public List<E> findChildElements(String key) {
+        Node<E> item = findNode(key);
+        return item == null ? new ArrayList<E>() : item.getChildElements();
+    }
+
+    /** 获取KEY对应元素的所有子元素 **/
+    public List<E> findAllDescendantElements(String key) {
+        return findAllDescendantElements(key, false);
+    }
+
+    /** 获取KEY对应元素的所有子元素, 可以指定是否包含指定KEY元素自身 **/
+    public List<E> findAllDescendantElements(String key, boolean includeSelf) {
+        Node<E> item = findNode(key);
+        List<E> elements = new ArrayList<>();
+        if (item == null) {
+            return elements;
+        }
+        if (includeSelf) {
+            elements.add(item.element);
+        }
+        elements.addAll(item.getAllDescendantElements());
+        return elements;
+    }
+
+    /** 未找到上级的KEY **/
+    public List<String> getKeysOfWithoutParent() {
+        return this.keysOfWithoutParent;
+    }
+
+    /** 广度优先遍历 **/
+    public Iterator<E> breadthFirstIterator() {
+        return container.breadthFirstElementIterator();
+    }
+
+    /** 深度度优先遍历 **/
+    public Iterator<E> depthFirstIterator() {
+        return container.depthFirstElementIterator();
+    }
+
+    public static class Node<T> {
+
+        /** 本级 **/
+        private final T element;
+        /** 上级 **/
+        private Node<T> parent;
+        /** 子级 **/
+        private final List<Node<T>> children;
+
+        public Node(T element) {
+            this.element = element;
+            this.children = new ArrayList<>();
+        }
+
+        public T getElement() {
+            return element;
+        }
+
+        /** 获取第一个子级元素 **/
+        public T getFirstElement() {
+            if (children.isEmpty()) {
+                return null;
+            } else {
+                return this.children.get(0).element;
+            }
+        }
+
+        /** 获取父级元素 **/
+        public T getParentElement() {
+            return parent == null ? null : parent.element;
+        }
+
+        /** 获取所有祖先元素 **/
+        public List<T> getAllAncestorElements() {
+            List<T> parents = new ArrayList<>();
+            Node<T> temp = parent;
+            while (temp != null && temp.element != null) {
+                parents.add(temp.element);
+                temp = temp.parent;
+            }
+            Collections.reverse(parents);
+            return parents;
+        }
+
+        /** 获取直接子级元素 **/
+        public List<T> getChildElements() {
+            List<T> elements = new ArrayList<>();
+            for (Node<T> item : this.children) {
+                elements.add(item.element);
+            }
+            return elements;
+        }
+
+        /** 增加子元素 **/
+        public void addChildElement(T child) {
+            addChildNode(new Node<>(child));
+        }
+
+        /** 获取后代元素 **/
+        public List<T> getAllDescendantElements() {
+            Iterator<T> iterator = depthFirstElementIterator();
+            List<T> elements = new ArrayList<>();
+            while (iterator.hasNext()) {
+                T next = iterator.next();
+                elements.add(next);
+            }
+            return elements;
+        }
+
+        /** 获取所有叶子元素 **/
+        public List<T> getAllLeafElements() {
+            Iterator<Node<T>> iterator = depthFirstNodeIterator();
+            List<T> elements = new ArrayList<>();
+            while (iterator.hasNext()) {
+                Node<T> next = iterator.next();
+                if (next.children.isEmpty()) {
+                    elements.add(next.element);
+                }
+            }
+            return elements;
+        }
+
+        /** 获取直接节点列表 **/
+        protected List<Node<T>> getChildNodes() {
+            return children;
+        }
+
+        /** 增加子级节点 **/
+        protected void addChildNode(Node<T> child) {
+            children.add(child);
+            child.parent = this;
+        }
+
+        /** 获取父级节点 **/
+        protected Node<T> getParentNode() {
+            return parent == null ? null : parent;
+        }
+
+        /** 广度优先遍历元素 **/
+        public Iterator<T> breadthFirstElementIterator() {
+            return new BreadthFirstElementIterator<>(new BreadthFirstNodeIterator<>(this));
+        }
+
+        /** 深度度优先遍历元素 **/
+        public Iterator<T> depthFirstElementIterator() {
+            return new DepthFirstElementIterator<>(new DepthFirstNodeIterator<>(this));
+        }
+
+        /** 广度优先遍历节点 **/
+        public Iterator<Node<T>> breadthFirstNodeIterator() {
+            return new BreadthFirstNodeIterator<>(this);
+        }
+
+        /** 深度度优先遍历节点 **/
+        public Iterator<Node<T>> depthFirstNodeIterator() {
+            return new DepthFirstNodeIterator<>(this);
+        }
+
+        @Override
+        public String toString() {
+            return element == null ? "{root}" : element.toString();
+        }
+    }
+
+    private static class BreadthFirstElementIterator<T> implements Iterator<T> {
+        private final BreadthFirstNodeIterator<T> iterator;
+
+        public BreadthFirstElementIterator(BreadthFirstNodeIterator<T> iterator) {
+            this.iterator = iterator;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public T next() {
+            Node<T> next = iterator.next();
+            return next == null ? null : next.element;
+        }
+
+        @Override
+        public void remove() {
+            iterator.remove();
+        }
+    }
+
+    private static class DepthFirstElementIterator<T> implements Iterator<T> {
+        private final DepthFirstNodeIterator<T> iterator;
+
+        public DepthFirstElementIterator(DepthFirstNodeIterator<T> iterator) {
+            this.iterator = iterator;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public T next() {
+            Node<T> next = iterator.next();
+            return next == null ? null : next.element;
+        }
+
+        @Override
+        public void remove() {
+            iterator.remove();
+        }
+    }
+
+    /**
+     * 广度优先遍历<br>
+     * 根节点自身不会参与遍历
+     *
+     * @author zhaohuihua
+     * @version 170507
+     */
+    private static class BreadthFirstNodeIterator<T> implements Iterator<Node<T>> {
+
+        private final Queue<Node<T>> queue;
+        private Node<T> current;
+
+        public BreadthFirstNodeIterator(Node<T> root) {
+            queue = new LinkedList<>();
+            if (!root.children.isEmpty()) {
+                for (Node<T> child : root.children) {
+                    queue.offer(child);
+                }
+            }
+        }
+
+        public boolean hasNext() {
+            return !queue.isEmpty();
+        }
+
+        public Node<T> next() {
+            Node<T> next = queue.poll();
+            if (next == null) {
+                current = null;
+                return null;
+            }
+            if (next.children != null) {
+                for (Node<T> i : next.children) {
+                    queue.offer(i);
+                }
+            }
+            current = next;
+            return next;
+        }
+
+        public void remove() {
+            if (current == null) {
+                throw new IllegalStateException("Call remove() must be after the next().");
+            }
+
+            // 先将当前节点从父节点的children中删除
+            current.parent.children.remove(current);
+            if (!current.children.isEmpty()) {
+                // 再把当前节点的子节点从队列中删除
+                // 因为当前节点的子节点在调用next()时已经加到遍历队列中了(孙子节点还没有加到队列中)
+                queue.removeAll(current.children);
+            }
+            current = null;
+        }
+    }
+
+    /**
+     * 深度优先遍历<br>
+     * 根节点自身不会参与遍历
+     *
+     * @author zhaohuihua
+     * @version 170801
+     */
+    private static class DepthFirstNodeIterator<T> implements Iterator<Node<T>> {
+
+        private final Queue<Node<T>> queue;
+        private Node<T> current;
+
+        public DepthFirstNodeIterator(Node<T> root) {
+            queue = new LinkedList<>();
+            if (!root.children.isEmpty()) {
+                for (Node<T> child : root.children) {
+                    recursiveOffer(child);
+                }
+            }
+        }
+
+        private void recursiveOffer(Node<T> self) {
+            queue.offer(self);
+            if (!self.children.isEmpty()) {
+                for (Node<T> child : self.children) {
+                    recursiveOffer(child);
+                }
+            }
+        }
+
+        public boolean hasNext() {
+            return !queue.isEmpty();
+        }
+
+        public Node<T> next() {
+            Node<T> next = queue.poll();
+            current = next;
+            return next;
+        }
+
+        public void remove() {
+            if (current == null) {
+                throw new IllegalStateException("Call remove() must be after the next().");
+            }
+
+            // 先将当前节点从父节点的children中删除
+            current.parent.children.remove(current);
+            if (!current.children.isEmpty()) {
+                // 将所有后代节点从队列中删除
+                removeAllChildren(current);
+            }
+            current = null;
+        }
+
+        // 将所有后代节点从队列中删除
+        private void removeAllChildren(Node<T> self) {
+            for (Node<T> child : self.children) {
+                queue.remove(child);
+                removeAllChildren(child);
+            }
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/debug/BaseDebugger.java b/able/src/main/java/com/gitee/qdbp/able/debug/BaseDebugger.java
index f8e2a6ab1013cb831fbad7b3a983f6d1671f0d19..678b411eff8e2d3b2b46e86d7e10fdcc5d66a972 100644
--- a/able/src/main/java/com/gitee/qdbp/able/debug/BaseDebugger.java
+++ b/able/src/main/java/com/gitee/qdbp/able/debug/BaseDebugger.java
@@ -1,5 +1,7 @@
 package com.gitee.qdbp.able.debug;
 
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
 import java.util.Date;
 import java.util.List;
 import com.gitee.qdbp.able.matches.StringMatcher;
@@ -44,15 +46,39 @@ public abstract class BaseDebugger implements Debugger {
         log(buffer.toString());
     }
 
+    @Override
+    public void debug(Formatted formatted) {
+        log(formatted.getMessage());
+    }
+
+    private static final String DECIMAL_FORMAT = "#.##################";
+
     protected void makeMessage(StringBuffer buffer, String format, Object... args) {
+        makeMessage(buffer, new Date(), format, args);
+    }
+
+    protected void makeMessage(StringBuffer buffer, Date time, String format, Object... args) {
         if (timePattern != null) {
-            buffer.append(DateTools.format(new Date(), timePattern)).append(' ');
+            buffer.append(DateTools.format(time, timePattern)).append(' ');
         }
         if (args == null || args.length == 0) {
             buffer.append(format);
         } else {
             String fmt = StringTools.replace(format, "{}", "%s");
-            buffer.append(String.format(fmt, args));
+            Object[] params = new String[args.length];
+            for (int i = 0; i < args.length; i++) {
+                Object item = args[i];
+                if (item instanceof Date) {
+                    params[i] = DateTools.toAutoString((Date) item);
+                } else if (item instanceof Double) {
+                    params[i] = new DecimalFormat(DECIMAL_FORMAT).format(item);
+                } else if (item instanceof BigDecimal) {
+                    params[i] = ConvertTools.toPlainString((BigDecimal) item);
+                } else {
+                    params[i] = item;
+                }
+            }
+            buffer.append(String.format(fmt, params));
             // 如果参数比占位符多, 且最后一个参数是异常类, 则输出异常堆栈
             Object last = args[args.length - 1];
             if (last instanceof Throwable) {
diff --git a/able/src/main/java/com/gitee/qdbp/able/debug/Debugger.java b/able/src/main/java/com/gitee/qdbp/able/debug/Debugger.java
index 3c4ed0bb46b4fa91fc2934aa0dada59eb77a070e..376af9562a8b331d842753eeb004ca1b54aed2fa 100644
--- a/able/src/main/java/com/gitee/qdbp/able/debug/Debugger.java
+++ b/able/src/main/java/com/gitee/qdbp/able/debug/Debugger.java
@@ -10,12 +10,32 @@ public interface Debugger {
 
     /**
      * 记录日志
-     * 
+     *
      * @param message 消息内容
      * @param args 占位符参数
      */
     void debug(String message, Object... args);
 
+    /**
+     * 记录日志
+     *
+     * @param formatted 已格式化的消息内容
+     */
+    void debug(Formatted formatted);
+
+    class Formatted {
+
+        private final String message;
+
+        public Formatted(String message) {
+            this.message = message;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+    }
+
     interface Aware {
 
         void setDebugger(Debugger debugger);
diff --git a/able/src/main/java/com/gitee/qdbp/able/enums/FileErrorCode.java b/able/src/main/java/com/gitee/qdbp/able/enums/FileErrorCode.java
index 8f5b571450f7cdd5dc58e51165c60d91c787eaa8..fbb1e98e1eb0bd090b3d0fe9b5b2bf84ef178a2a 100644
--- a/able/src/main/java/com/gitee/qdbp/able/enums/FileErrorCode.java
+++ b/able/src/main/java/com/gitee/qdbp/able/enums/FileErrorCode.java
@@ -15,12 +15,39 @@ public enum FileErrorCode implements IResultMessage {
     FILE_NOT_FOUND,
     /** 文件读取失败 **/
     FILE_READ_ERROR,
+    /** 文件查询失败 **/
+    FILE_QUERY_ERROR,
     /** 文件写入失败 **/
     FILE_WRITE_ERROR,
+    /** 文件保存失败 **/
+    FILE_SAVE_ERROR,
+    /** 文件导入失败 **/
+    FILE_IMPORT_ERROR,
+    /** 文件导出失败 **/
+    FILE_EXPORT_ERROR,
+    /** 文件上传失败 **/
+    FILE_UPLOAD_ERROR,
+    /** 文件下载失败 **/
+    FILE_DOWNLOAD_ERROR,
+    /** 文件重命名失败 **/
+    FILE_RENAME_ERROR,
+    /** 文件删除失败 **/
+    FILE_DELETE_ERROR,
+    /** 文件处理失败 **/
+    FILE_HANDLE_ERROR,
+
+    /** 文件模板格式错误 **/
+    FILE_TEMPLATE_ERROR,
     /** 文件格式错误 **/
     FILE_FORMAT_ERROR,
-    /** 文件模板格式错误 **/
-    FILE_TEMPLATE_ERROR;
+    /** 文件路径错误 **/
+    FILE_PATH_ERROR,
+    /** 文件大小超出限制 **/
+    FILE_SIZE_OVER_LIMIT,
+    /** 缺少文件扩展名 **/
+    FILE_EXTENSION_LOSE,
+    /** 不支持的文件类型; **/
+    FILE_EXTENSION_ERROR;
 
     /** {@inheritDoc} **/
     @Override
diff --git a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
index 2c743985c17c20b9a61e492a76f6c86cb7a65957..a98e3ae09ee217dbce5050239df3ca56d9fb365a 100644
--- a/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
+++ b/able/src/main/java/com/gitee/qdbp/able/exception/ServiceException.java
@@ -2,7 +2,9 @@ package com.gitee.qdbp.able.exception;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.UndeclaredThrowableException;
+import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.IResultMessage;
+import com.gitee.qdbp.tools.utils.StringTools;
 
 /**
  * 业务异常类
@@ -10,7 +12,7 @@ import com.gitee.qdbp.able.result.IResultMessage;
  * @author zhaohuihua
  * @version 150915
  */
-public class ServiceException extends RuntimeException implements IResultMessage {
+public class ServiceException extends RuntimeException implements IResultException {
 
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
@@ -30,8 +32,11 @@ public class ServiceException extends RuntimeException implements IResultMessage
      * @param result 错误返回码
      */
     public ServiceException(IResultMessage result) {
-        super(result.getMessage());
+        super(getRealMessage(result));
         this.code = result.getCode();
+        if (result instanceof IResultException) {
+            this.details = ((IResultException) result).getDetails();
+        }
     }
 
     /**
@@ -39,9 +44,11 @@ public class ServiceException extends RuntimeException implements IResultMessage
      *
      * @param result 错误返回码
      * @param details 错误详情
+     * @deprecated 第2个参数含义容易误解为message, 改为throw new ServiceException(...).setDetails(...);
      */
+    @Deprecated
     public ServiceException(IResultMessage result, String details) {
-        super(result.getMessage());
+        super(getRealMessage(result));
         this.code = result.getCode();
         this.details = details;
     }
@@ -53,8 +60,11 @@ public class ServiceException extends RuntimeException implements IResultMessage
      * @param cause 引发异常的原因
      */
     public ServiceException(IResultMessage result, Throwable cause) {
-        super(result.getMessage(), cause);
+        super(getRealMessage(result), cause);
         this.code = result.getCode();
+        if (result instanceof IResultException) {
+            this.details = ((IResultException) result).getDetails();
+        }
     }
 
     /**
@@ -63,9 +73,11 @@ public class ServiceException extends RuntimeException implements IResultMessage
      * @param result 错误返回码
      * @param details 错误详情
      * @param cause 引发异常的原因
+     * @deprecated 第2个参数含义容易误解为message, 改为throw new ServiceException(...).setDetails(...);
      */
+    @Deprecated
     public ServiceException(IResultMessage result, String details, Throwable cause) {
-        super(result.getMessage(), cause);
+        super(getRealMessage(result), cause);
         this.code = result.getCode();
         this.details = details;
     }
@@ -85,8 +97,9 @@ public class ServiceException extends RuntimeException implements IResultMessage
      * 
      * @param code 错误返回码
      */
-    public void setCode(String code) {
+    public ServiceException setCode(String code) {
         this.code = code;
+        return this;
     }
 
     /**
@@ -94,6 +107,7 @@ public class ServiceException extends RuntimeException implements IResultMessage
      *
      * @return 错误详情
      */
+    @Override
     public String getDetails() {
         return details;
     }
@@ -103,8 +117,27 @@ public class ServiceException extends RuntimeException implements IResultMessage
      * 
      * @param details 错误详情
      */
-    public void setDetails(String details) {
+    public ServiceException setDetails(String details) {
         this.details = details;
+        return this;
+    }
+
+    /**
+     * 设置错误详情
+     *
+     * @param pattern 错误消息
+     * @param args 替换参数
+     * @return 对象自身
+     */
+    public ServiceException setDetails(String pattern, Object... args) {
+        if (pattern == null || pattern.length() == 0 || args == null || args.length == 0) {
+            this.details = pattern;
+            return this;
+        } else {
+            String fmt = StringTools.replace(pattern, "{}", "%s");
+            this.details = String.format(fmt, args);
+            return this;
+        }
     }
 
     /**
@@ -112,6 +145,7 @@ public class ServiceException extends RuntimeException implements IResultMessage
      *
      * @return 错误消息
      */
+    @Override
     public String getSimpleMessage() {
         return super.getMessage();
     }
@@ -145,10 +179,26 @@ public class ServiceException extends RuntimeException implements IResultMessage
         }
     }
 
-    /** 从exception.cause中查找ServiceException **/
+    private static String getRealMessage(IResultMessage result) {
+        if (result instanceof IResultException) {
+            return ((IResultException) result).getSimpleMessage();
+        } else {
+            return result.getMessage();
+        }
+    }
+
+    /** 从exception.cause中查找ServiceException, 如果存在则抛出该异常 **/
     public static void throwWhenServiceException(Throwable e) throws ServiceException {
+        ServiceException exception = findWhenServiceException(e);
+        if (exception != null) {
+            throw exception;
+        }
+    }
+
+    /** 从exception.cause中查找ServiceException, 如果不存在则返回null **/
+    public static ServiceException findWhenServiceException(Throwable e) throws ServiceException {
         Throwable unwrapped = e;
-        while (true) {
+        while (unwrapped != null) {
             if (unwrapped instanceof InvocationTargetException) {
                 unwrapped = ((InvocationTargetException) unwrapped).getTargetException();
             } else if (unwrapped instanceof UndeclaredThrowableException) {
@@ -159,11 +209,15 @@ public class ServiceException extends RuntimeException implements IResultMessage
         }
 
         Throwable cause = unwrapped;
+        if (cause == e) {
+            cause = e.getCause();
+        }
         while (cause != null) {
             if (cause instanceof ServiceException) {
-                throw (ServiceException) cause;
+                return (ServiceException) cause;
             }
             cause = cause.getCause();
         }
+        return null;
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BaseConsumer.java b/able/src/main/java/com/gitee/qdbp/able/function/BaseConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7bd0691260e271157c2767aea8319133a491aa0
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BaseConsumer.java
@@ -0,0 +1,21 @@
+package com.gitee.qdbp.able.function;
+
+/**
+ * JDK8中的Consumer在7中的替换接口<br>
+ * Represents an operation that accepts a single input argument and returns no
+ * result. Unlike most other functional interfaces, {@code Consumer} is expected
+ * to operate via side-effects.
+ *
+ * <p>This is a functional interface, whose functional method is {@link #accept(Object)}.
+ *
+ * @param <T> the type of the input to the operation
+ */
+public interface BaseConsumer<T> {
+
+    /**
+     * Performs this operation on the given argument.
+     *
+     * @param t the input argument
+     */
+    void accept(T t);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java b/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java
new file mode 100644
index 0000000000000000000000000000000000000000..5a29ace56dffe2b4ed3c9189b9d4e70c365bc4ed
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BaseFunction.java
@@ -0,0 +1,21 @@
+package com.gitee.qdbp.able.function;
+
+/**
+ * JDK8中的Function在7中的替换接口<br>
+ * Represents a function that accepts one argument and produces a result.
+ *
+ * <p>This is a functional interface, whose functional method is {@link #apply(Object)}.
+ *
+ * @param <T> the type of the input to the function
+ * @param <R> the type of the result of the function
+ */
+public interface BaseFunction<T, R> {
+
+    /**
+     * Applies this function to the given argument.
+     *
+     * @param t the function argument
+     * @return the function result
+     */
+    R apply(T t);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java b/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java
new file mode 100644
index 0000000000000000000000000000000000000000..b906d995feba35ae5243371cc0960e8ecead0b15
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BaseSupplier.java
@@ -0,0 +1,22 @@
+package com.gitee.qdbp.able.function;
+
+/**
+ * JDK8中的Supplier在7中的替换接口<br>
+ * Represents a supplier of results.
+ *
+ * <p>There is no requirement that a new or distinct result be returned each
+ * time the supplier is invoked.
+ *
+ * <p>This is a functional interface, whose functional method is {@link #get()}.
+ *
+ * @param <T> the type of results supplied by this supplier
+ */
+public interface BaseSupplier<T> {
+
+    /**
+     * Gets a result.
+     *
+     * @return a result
+     */
+    T get();
+}
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BinaryConsumer.java b/able/src/main/java/com/gitee/qdbp/able/function/BinaryConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..287bf3d8ba634f48c0580c0eb609dba881dc5bd4
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BinaryConsumer.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.able.function;
+
+import java.util.function.Consumer;
+
+/**
+ * JDK8中的BiConsumer在7中的替换接口<br>
+ * Represents an operation that accepts two input arguments and returns no
+ * result.  This is the two-arity specialization of {@link Consumer}.
+ * Unlike most other functional interfaces, {@code BiConsumer} is expected
+ * to operate via side-effects.
+ *
+ * <p>This is a <a href="package-summary.html">functional interface</a>
+ * whose functional method is {@link #accept(Object, Object)}.</p>
+ *
+ * @param <T> the type of the first argument to the operation
+ * @param <U> the type of the second argument to the operation
+ */
+public interface BinaryConsumer<T, U> {
+
+    /**
+     * Performs this operation on the given arguments.
+     *
+     * @param t the first input argument
+     * @param u the second input argument
+     */
+    void accept(T t, U u);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/function/BinaryFunction.java b/able/src/main/java/com/gitee/qdbp/able/function/BinaryFunction.java
new file mode 100644
index 0000000000000000000000000000000000000000..3f838709eb75b7db2552f3b150a7370705156718
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/function/BinaryFunction.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.able.function;
+
+import java.util.function.Function;
+
+/**
+ * JDK8中的BiFunction在7中的替换接口<br>
+ * Represents a function that accepts two arguments and produces a result.
+ * This is the two-arity specialization of {@link Function}.
+ *
+ * <p>This is a <a href="package-summary.html">functional interface</a>
+ * whose functional method is {@link #apply(Object, Object)}.</p>
+ *
+ * @param <T> the type of the first argument to the function
+ * @param <U> the type of the second argument to the function
+ * @param <R> the type of the result of the function
+ */
+public interface BinaryFunction<T, U, R> {
+
+    /**
+     * Applies this function to the given arguments.
+     *
+     * @param t the first function argument
+     * @param u the second function argument
+     * @return the function result
+     */
+    R apply(T t, U u);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/instance/WithExtensionFileFilter.java b/able/src/main/java/com/gitee/qdbp/able/instance/WithExtensionFileFilter.java
index 1f131ba72407829dd9b13ded46a34710566992eb..dd5f0e780da83d7a65c5f22a961337445d2e2442 100644
--- a/able/src/main/java/com/gitee/qdbp/able/instance/WithExtensionFileFilter.java
+++ b/able/src/main/java/com/gitee/qdbp/able/instance/WithExtensionFileFilter.java
@@ -59,7 +59,7 @@ public class WithExtensionFileFilter implements FileFilter, Serializable {
         this.include = include;
         this.extensions = new HashMap<>();
         for (String extension : extensions) {
-            this.extensions.put(StringTools.removeLeft(extension.toLowerCase(), '.'), null);
+            this.extensions.put(StringTools.trimLeft(extension.toLowerCase(), '.'), null);
         }
     }
 
diff --git a/able/src/main/java/com/gitee/qdbp/able/instance/WithMatcherFileFilter.java b/able/src/main/java/com/gitee/qdbp/able/instance/WithMatcherFileFilter.java
index 475611814e540a5a8596bb14916a17f1d291c592..aee256febf4aa9860b2f2ac5721fb75eb0a3c0b4 100644
--- a/able/src/main/java/com/gitee/qdbp/able/instance/WithMatcherFileFilter.java
+++ b/able/src/main/java/com/gitee/qdbp/able/instance/WithMatcherFileFilter.java
@@ -3,9 +3,8 @@ package com.gitee.qdbp.able.instance;
 import java.io.File;
 import java.io.FileFilter;
 import java.io.Serializable;
-import com.gitee.qdbp.able.matches.StringMatcher;
-import com.gitee.qdbp.able.matches.WrapStringMatcher;
-import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.able.matches.FileMatcher;
+import com.gitee.qdbp.able.matches.WrapFileMatcher;
 
 /**
  * 根据StringMatcher筛选文件的过滤器
@@ -18,9 +17,7 @@ public class WithMatcherFileFilter implements FileFilter, Serializable {
     /** serialVersionUID **/
     private static final long serialVersionUID = 1L;
     /** 匹配规则 **/
-    private StringMatcher matcher;
-    /** 匹配文件路径还是文件名 **/
-    private boolean usePath = true;
+    private FileMatcher matcher;
 
     /** 默认构造函数 **/
     public WithMatcherFileFilter() {
@@ -33,76 +30,44 @@ public class WithMatcherFileFilter implements FileFilter, Serializable {
      * equals:开头的解析为EqualsStringMatcher<br>
      * contains:开头的解析为ContainsStringMatcher<br>
      * 其余的也解析为ContainsStringMatcher<br>
-     * 
+     *
      * @param matcher 文件名匹配规则
      */
     public WithMatcherFileFilter(String matcher) {
-        this(WrapStringMatcher.parseMatcher(matcher, false));
+        this(WrapFileMatcher.parseMatcher(matcher, false));
     }
 
     /**
      * 构造函数
-     * 
+     *
      * @param matcher 文件名匹配规则
      */
-    public WithMatcherFileFilter(StringMatcher matcher) {
+    public WithMatcherFileFilter(FileMatcher matcher) {
         this.matcher = matcher;
     }
 
     @Override
     public boolean accept(File file) {
-        boolean isFolder = file.isDirectory();
-        if (isFolder) {
-            return false;
-        } else if (matcher == null) {
+        if (matcher == null) {
             // 未设置文件匹配规则就等于遍历所有文件, 因此返回true
             return true;
         }
-        String path;
-        if (!usePath) {
-            path = file.getName();
-        } else {
-            // 路径转换为/分隔符, 方便windows/linux统一处理
-            path = PathTools.formatPath(file.getAbsolutePath());
-            if (!path.startsWith("/") && path.charAt(1) == ':') {
-                // windows文件, 去掉盘符, 如D:/home/files, 只取/home/files
-                // 如果保留盘符, ant规则不好处理
-                path = path.substring(2);
-            }
-        }
-        return matcher.matches(path);
+        return matcher.matches(file);
     }
 
-    /** 文件名匹配规则 **/
-    public StringMatcher getMatcher() {
+    /** 文件匹配规则 **/
+    public FileMatcher getMatcher() {
         return matcher;
     }
 
-    /** 文件名匹配规则 **/
-    public void setMatcher(StringMatcher matcher) {
+    /** 文件匹配规则 **/
+    public void setMatcher(FileMatcher matcher) {
         this.matcher = matcher;
     }
 
-    /** 匹配文件路径还是文件名 **/
-    public boolean isUsePath() {
-        return usePath;
-    }
-
-    /** 匹配文件路径还是文件名 **/
-    public void setUsePath(boolean usePath) {
-        this.usePath = usePath;
-    }
-
     @Override
     public String toString() {
-        if (matcher == null) {
-            return "matcher=null";
-        }
-        StringBuilder buffer = new StringBuilder();
-        buffer.append("matcher").append('=').append(matcher);
-        buffer.append(',').append(' ');
-        buffer.append("usePath").append('=').append(usePath);
-        return buffer.toString();
+        return matcher == null ? "matcher=null" : matcher.toString();
     }
 
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java b/able/src/main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java
index 3fd1695ea87fc02566eb3685acec73c9c03cfaee..4e0bfae5bacc937b537cffd29a266192cb04e6e9 100644
--- a/able/src/main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java
+++ b/able/src/main/java/com/gitee/qdbp/able/jdbc/condition/DbWhere.java
@@ -30,7 +30,7 @@ import com.gitee.qdbp.able.jdbc.base.DbCondition;
     where.on("userName").like(entity.getUserName());
     // [ORACLE/DB2] AND USER_NAME NOT LIKE('%'||:$1||'%')
     // [MYSQL] AND USER_NAME NOT LIKE CONCAT('%',:$1,'%')
-    where.on("userName").notLkie(entity.getUserName());
+    where.on("userName").notLike(entity.getUserName());
     // [ORACLE/DB2] AND PHONE LIKE(:$1||'%')
     // [MYSQL] AND PHONE LIKE CONCAT(:$1,'%')
     where.on("phone").startsWith("139");
diff --git a/able/src/main/java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java b/able/src/main/java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java
index 90a13d681a72509578995a4ba4b0eb6306e4b6df..773ef5477a44c3a4ae70a215a3423a55f21d19d0 100644
--- a/able/src/main/java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java
+++ b/able/src/main/java/com/gitee/qdbp/able/jdbc/model/DbFieldValue.java
@@ -29,7 +29,7 @@ public class DbFieldValue implements Serializable {
         return fieldValue;
     }
 
-    public void setFieldValue(String fieldValue) {
+    public void setFieldValue(Object fieldValue) {
         this.fieldValue = fieldValue;
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/AntFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/AntFileMatcher.java
index 8362eb930931be6d12e8dc3dfe17945e667f0c87..ad75b91512c72a6491b21f00ecc3b449df00fa65 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/AntFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/AntFileMatcher.java
@@ -31,4 +31,20 @@ public class AntFileMatcher extends BaseFileMatcher {
     public AntFileMatcher(String pattern, Target target, Matches mode) {
         super(new AntStringMatcher(pattern, true, mode), target);
     }
+
+    public static AntFileMatcher ofNameMatches(String pattern) {
+        return new AntFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static AntFileMatcher ofPathMatches(String pattern) {
+        return new AntFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static AntFileMatcher ofNameNotMatches(String pattern) {
+        return new AntFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static AntFileMatcher ofPathNotMatches(String pattern) {
+        return new AntFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/EndsFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/EndsFileMatcher.java
index 7b362153d696cea4cc44452a088f60b98b06508a..75ff976654010b2a889436d0d70babaec61b11a5 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/EndsFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/EndsFileMatcher.java
@@ -32,4 +32,19 @@ public class EndsFileMatcher extends BaseFileMatcher {
         super(new EndsStringMatcher(pattern, mode), target);
     }
 
+    public static EndsFileMatcher ofNameMatches(String pattern) {
+        return new EndsFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static EndsFileMatcher ofPathMatches(String pattern) {
+        return new EndsFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static EndsFileMatcher ofNameNotMatches(String pattern) {
+        return new EndsFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static EndsFileMatcher ofPathNotMatches(String pattern) {
+        return new EndsFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/EqualsFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/EqualsFileMatcher.java
index 8a320f8d10ce4da64ebfacb1ba3de755427b5427..4a8f3b13b71bbb33805a8ca6ec15a50c70cfac19 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/EqualsFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/EqualsFileMatcher.java
@@ -31,4 +31,20 @@ public class EqualsFileMatcher extends BaseFileMatcher {
     public EqualsFileMatcher(String pattern, Target target, Matches mode) {
         super(new EqualsStringMatcher(pattern, mode), target);
     }
+
+    public static EqualsFileMatcher ofNameMatches(String pattern) {
+        return new EqualsFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static EqualsFileMatcher ofPathMatches(String pattern) {
+        return new EqualsFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static EqualsFileMatcher ofNameNotMatches(String pattern) {
+        return new EqualsFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static EqualsFileMatcher ofPathNotMatches(String pattern) {
+        return new EqualsFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/ExtensionFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/ExtensionFileMatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..f718f3536569b3f3b4c49b4b61cd02841eeb0415
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/ExtensionFileMatcher.java
@@ -0,0 +1,65 @@
+package com.gitee.qdbp.able.matches;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.matches.StringMatcher.Matches;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 文件后缀匹配规则<br>
+ * new ExtensionFileMatcher("doc,docx,xls,xlsx");<br>
+ *
+ * @author zhaohuihua
+ * @version 20230324
+ */
+public class ExtensionFileMatcher implements FileMatcher {
+
+    /** 是否反转判断结果 **/
+    private final boolean reverse;
+    private final String pattern;
+    private final Map<String, ?> extensions = new HashMap<>();
+
+    public ExtensionFileMatcher(String extensions) {
+        this(extensions, Matches.Positive);
+    }
+
+    public ExtensionFileMatcher(String extensions, Matches mode) {
+        this.reverse = mode == Matches.Negative;
+        this.pattern = extensions;
+        List<String> items = StringTools.splits(extensions, ',');
+        for (String extension : items) {
+            this.extensions.put(extension.toLowerCase(), null);
+        }
+    }
+
+    public ExtensionFileMatcher(List<String> extensions) {
+        this(extensions, Matches.Positive);
+    }
+
+    public ExtensionFileMatcher(List<String> extensions, Matches mode) {
+        this.reverse = mode == Matches.Negative;
+        this.pattern = ConvertTools.joinToString(extensions);
+        for (String extension : extensions) {
+            this.extensions.put(extension.toLowerCase(), null);
+        }
+    }
+
+    @Override
+    public boolean matches(File source) {
+        String extension = PathTools.getExtension(source.getName(), false).toLowerCase();
+        return this.extensions.containsKey(extension) != reverse;
+    }
+
+    @Override
+    public String toString() {
+        if (reverse) {
+            return "extension!:" + pattern;
+        } else {
+            return "extension:" + pattern;
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
new file mode 100644
index 0000000000000000000000000000000000000000..0f8f817b3300fbe50d72e19ce18dc224be8eac96
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractor.java
@@ -0,0 +1,14 @@
+package com.gitee.qdbp.able.matches;
+
+/**
+ * 字符串组提取接口
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public interface GroupExtractor {
+
+    GroupSubString extractFirst(String string);
+
+    GroupSubStrings extractAll(String string);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
new file mode 100644
index 0000000000000000000000000000000000000000..b550096609daaa60012b57313558348d42114985
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupExtractors.java
@@ -0,0 +1,57 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.List;
+
+/**
+ * 字符串组提取集合实现类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class GroupExtractors implements GroupExtractor {
+    private final List<? extends GroupExtractor> extractors;
+
+    public <T extends GroupExtractor> GroupExtractors(List<T> extractors) {
+        this.extractors = extractors;
+    }
+
+    public List<? extends GroupExtractor> getExtractors() {
+        return extractors;
+    }
+
+    @Override
+    public GroupSubString extractFirst(String string) {
+        for (int i = 0, z = extractors.size(); i < z; i++) {
+            GroupExtractor extractor = extractors.get(i);
+            GroupSubString result = extractor.extractFirst(string);
+            if (result != null) {
+                if (!result.containsExtra("index")) {
+                    result.putExtra("index", i);
+                }
+                if (!result.containsExtra("rule")) {
+                    result.putExtra("rule", extractor);
+                }
+                return result;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public GroupSubStrings extractAll(String string) {
+        for (int i = 0, z = extractors.size(); i < z; i++) {
+            GroupExtractor extractor = extractors.get(i);
+            GroupSubStrings result = extractor.extractAll(string);
+            if (result != null) {
+                if (!result.containsExtra("index")) {
+                    result.putExtra("index", i);
+                }
+                if (!result.containsExtra("rule")) {
+                    result.putExtra("rule", extractor);
+                }
+                return result;
+            }
+        }
+        return null;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
new file mode 100644
index 0000000000000000000000000000000000000000..a87087f78c9bd39e030348a66d568c03e29277eb
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubString.java
@@ -0,0 +1,114 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.HashMap;
+import java.util.Map;
+import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.able.model.reusable.ExtraBase;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+
+/**
+ * 带分组的子字符串对象
+ *
+ * @author zhaohuihua
+ * @version 20220129
+ */
+public class GroupSubString extends HashMap<String, SubString> implements ExtraBase {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    private ExtraData extraData;
+
+    /** 匹配项内容[即group(0)] **/
+    private String matchContent;
+    /** 匹配项在原文本的开始位置 **/
+    private int matchStartIndex;
+    /** 匹配项在原文本的结束位置 **/
+    private int matchEndIndex;
+
+    /** 匹配项内容[即group(0)] **/
+    public String getMatchContent() {
+        return matchContent;
+    }
+
+    /** 匹配项内容[即group(0)] **/
+    public void setMatchContent(String matchContent) {
+        this.matchContent = matchContent;
+    }
+
+    /** 匹配项在原文本的开始位置 **/
+    public int getMatchStartIndex() {
+        return matchStartIndex;
+    }
+
+    /** 匹配项在原文本的开始位置 **/
+    public void setMatchStartIndex(int matchStartIndex) {
+        this.matchStartIndex = matchStartIndex;
+    }
+
+    /** 匹配项在原文本的结束位置 **/
+    public int getMatchEndIndex() {
+        return matchEndIndex;
+    }
+
+    /** 匹配项在原文本的结束位置 **/
+    public void setMatchEndIndex(int matchEndIndex) {
+        this.matchEndIndex = matchEndIndex;
+    }
+
+    public SubString get(Integer key) {
+        return super.get(String.valueOf(key));
+    }
+
+    /** 获取附加数据 **/
+    @Override
+    public Map<String, Object> getExtra() {
+        return extraData == null ? null : extraData.getExtra();
+    }
+
+    /** 设置附加数据 **/
+    @Override
+    public void setExtra(Map<String, Object> extra) {
+        this.extraData = new ExtraData(extra);
+    }
+
+    /** 是否存在指定的附加数据 **/
+    @Override
+    public boolean containsExtra(String key) {
+        return extraData != null && extraData.containsExtra(key);
+    }
+
+    /** 获取指定的附加数据 **/
+    @Override
+    public Object getExtra(String key) {
+        return extraData == null ? null : extraData.getExtra(key);
+    }
+
+    /** 获取指定类型的附加数据 **/
+    @Override
+    public <T> T getExtra(String key, Class<T> clazz) {
+        return extraData == null ? null : extraData.getExtra(key, clazz);
+    }
+
+    /** 设置指定的附加数据 **/
+    @Override
+    public void putExtra(String key, Object value) {
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(key, value);
+    }
+
+    /** 设置附加信息 **/
+    @Override
+    public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(extra);
+    }
+
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubStrings.java b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubStrings.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8f616cbd547ea5895855edd3e330f493c894f3a
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/GroupSubStrings.java
@@ -0,0 +1,72 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.Map;
+import com.gitee.qdbp.able.model.reusable.ExtraBase;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+
+/**
+ * 带分组的子字符串对象集合
+ *
+ * @author zhaohuihua
+ * @version 20220129
+ */
+public class GroupSubStrings extends ArrayList<GroupSubString> implements ExtraBase {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    private ExtraData extraData;
+
+    /** 获取附加数据 **/
+    @Override
+    public Map<String, Object> getExtra() {
+        return extraData == null ? null : extraData.getExtra();
+    }
+
+    /** 设置附加数据 **/
+    @Override
+    public void setExtra(Map<String, Object> extra) {
+        this.extraData = new ExtraData(extra);
+    }
+
+    /** 是否存在指定的附加数据 **/
+    @Override
+    public boolean containsExtra(String key) {
+        return extraData != null && extraData.containsExtra(key);
+    }
+
+    /** 获取指定的附加数据 **/
+    @Override
+    public Object getExtra(String key) {
+        return extraData == null ? null : extraData.getExtra(key);
+    }
+
+    /** 获取指定类型的附加数据 **/
+    @Override
+    public <T> T getExtra(String key, Class<T> clazz) {
+        return extraData == null ? null : extraData.getExtra(key, clazz);
+    }
+
+    /** 设置指定的附加数据 **/
+    @Override
+    public void putExtra(String key, Object value) {
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(key, value);
+    }
+
+    /** 设置附加信息 **/
+    @Override
+    public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(extra);
+    }
+
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa26de34fa02ca7acf101d944c79827ba2fceac6
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubString.java
@@ -0,0 +1,115 @@
+package com.gitee.qdbp.able.matches;
+
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+import com.gitee.qdbp.tools.compare.CompareTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 带有位置描述信息的子字符串
+ *
+ * @author zhaohuihua
+ * @version 20220306
+ */
+public class LocatedSubString extends ExtraData implements Comparable<LocatedSubString> {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    /** 提取项内容 **/
+    private String groupContent;
+    /** 提取项在原文本的开始位置 **/
+    private int groupStartIndex;
+    /** 提取项在原文本的结束位置 **/
+    private int groupEndIndex;
+    /** 匹配项内容[即group(0)] **/
+    private String matchContent;
+    /** 匹配项在原文本的开始位置 **/
+    private int matchStartIndex;
+    /** 匹配项在原文本的结束位置 **/
+    private int matchEndIndex;
+
+    public LocatedSubString() {
+    }
+
+    /** 提取项内容 **/
+    public String getGroupContent() {
+        return groupContent;
+    }
+
+    /** 提取项内容 **/
+    public void setGroupContent(String groupContent) {
+        this.groupContent = groupContent;
+    }
+
+    /** 提取项在原文本的开始位置 **/
+    public int getGroupStartIndex() {
+        return groupStartIndex;
+    }
+
+    /** 提取项在原文本的开始位置 **/
+    public void setGroupStartIndex(int groupStartIndex) {
+        this.groupStartIndex = groupStartIndex;
+    }
+
+    /** 提取项在原文本的结束位置 **/
+    public int getGroupEndIndex() {
+        return groupEndIndex;
+    }
+
+    /** 提取项在原文本的结束位置 **/
+    public void setGroupEndIndex(int groupEndIndex) {
+        this.groupEndIndex = groupEndIndex;
+    }
+
+    /** 子文本内容[即group(0)] **/
+    public String getMatchContent() {
+        return matchContent;
+    }
+
+    /** 匹配项内容[即group(0)] **/
+    public void setMatchContent(String matchContent) {
+        this.matchContent = matchContent;
+    }
+
+    /** 匹配项在原文本的开始位置 **/
+    public int getMatchStartIndex() {
+        return matchStartIndex;
+    }
+
+    /** 匹配项在原文本的开始位置 **/
+    public void setMatchStartIndex(int matchStartIndex) {
+        this.matchStartIndex = matchStartIndex;
+    }
+
+    /** 匹配项在原文本的结束位置 **/
+    public int getMatchEndIndex() {
+        return matchEndIndex;
+    }
+
+    /** 匹配项在原文本的结束位置 **/
+    public void setMatchEndIndex(int matchEndIndex) {
+        this.matchEndIndex = matchEndIndex;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append(StringTools.pad(groupStartIndex, 3));
+        buffer.append(':').append(StringTools.pad(groupEndIndex, 3));
+        buffer.append("    ").append(groupContent);
+        return buffer.toString();
+    }
+
+    @Override
+    public int compareTo(LocatedSubString o) {
+        if (o == null) {
+            return -1;
+        }
+        return CompareTools.stream()
+                .asc(groupStartIndex, o.getGroupEndIndex())
+                .asc(groupEndIndex, o.getGroupEndIndex())
+                .asc(matchStartIndex, o.getMatchStartIndex())
+                .asc(matchEndIndex, o.getMatchEndIndex())
+                .compare();
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubStrings.java b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubStrings.java
new file mode 100644
index 0000000000000000000000000000000000000000..af9919e7f44d38050c955e74e7c8c1d5d0a4bba4
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/LocatedSubStrings.java
@@ -0,0 +1,71 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.Map;
+import com.gitee.qdbp.able.model.reusable.ExtraBase;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+
+/**
+ * 带有位置信息的子文本集合
+ *
+ * @author zhaohuihua
+ * @version 20220306
+ */
+public class LocatedSubStrings extends ArrayList<LocatedSubString> implements ExtraBase {
+
+    /** serialVersionUID **/
+    private static final long serialVersionUID = 1L;
+
+    private ExtraData extraData;
+
+    /** 获取附加数据 **/
+    @Override
+    public Map<String, Object> getExtra() {
+        return extraData == null ? null : extraData.getExtra();
+    }
+
+    /** 设置附加数据 **/
+    @Override
+    public void setExtra(Map<String, Object> extra) {
+        this.extraData = new ExtraData(extra);
+    }
+
+    /** 是否存在指定的附加数据 **/
+    @Override
+    public boolean containsExtra(String key) {
+        return extraData != null && extraData.containsExtra(key);
+    }
+
+    /** 获取指定的附加数据 **/
+    @Override
+    public Object getExtra(String key) {
+        return extraData == null ? null : extraData.getExtra(key);
+    }
+
+    /** 获取指定类型的附加数据 **/
+    @Override
+    public <T> T getExtra(String key, Class<T> clazz) {
+        return extraData == null ? null : extraData.getExtra(key, clazz);
+    }
+
+    /** 设置指定的附加数据 **/
+    @Override
+    public void putExtra(String key, Object value) {
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(key, value);
+    }
+
+    /** 设置附加信息 **/
+    @Override
+    public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
+        if (this.extraData == null) {
+            this.extraData = new ExtraData();
+        }
+        this.extraData.putExtra(extra);
+    }
+}
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/QuotationReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/QuotationReplacer.java
index a507355c4cd95656f4902e6e0332637bfdde789e..7939b25e58fc0752442915597da9625f6972d542 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/QuotationReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/QuotationReplacer.java
@@ -38,7 +38,7 @@ public class QuotationReplacer implements StringReplacer {
     @Override
     public String toString() {
         StringBuilder buffer = new StringBuilder();
-        buffer.append("quotation: ").append(source).append(" -->");
+        buffer.append("quotation: ").append(source).append(" -- ");
         for (char c : targets) {
             buffer.append(' ').append(c);
         }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpFileMatcher.java
index 86f3304b017b368605cdf1496e95fb456f635ea2..4ac65dcefbe729f36664a98d11363aec9c95fad1 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpFileMatcher.java
@@ -53,4 +53,20 @@ public class RegexpFileMatcher extends BaseFileMatcher {
     public RegexpFileMatcher(Pattern pattern, Target target, Matches mode) {
         super(new RegexpStringMatcher(pattern, mode), target);
     }
+
+    public static RegexpFileMatcher ofNameMatches(String pattern) {
+        return new RegexpFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static RegexpFileMatcher ofPathMatches(String pattern) {
+        return new RegexpFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static RegexpFileMatcher ofNameNotMatches(String pattern) {
+        return new RegexpFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static RegexpFileMatcher ofPathNotMatches(String pattern) {
+        return new RegexpFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c7e97b8c0ef4364d8407c3d0a08ebc0f8c7a46b
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpGroupExtractor.java
@@ -0,0 +1,378 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import com.gitee.qdbp.able.beans.SubString;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.IndentTools;
+import com.gitee.qdbp.tools.utils.RuleTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * 正则表达式字符串组提取实现类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class RegexpGroupExtractor extends ExtraData implements GroupExtractor {
+
+    private static final long serialVersionUID = 2523069377411632793L;
+    /** 提取规则 **/
+    private Pattern pattern;
+    /** 排除规则 **/
+    private StringMatcher exclude;
+    /** 提取组序号 **/
+    private List<Integer> digitGroups;
+    /** 提取组名称 **/
+    private List<String> namedGroups;
+
+    public RegexpGroupExtractor() {
+    }
+
+    public RegexpGroupExtractor(String pattern, int... groups) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = Pattern.compile(pattern);
+        this.namedGroups = null;
+        this.digitGroups = toIntegerList(groups);
+    }
+
+    public RegexpGroupExtractor(Pattern pattern, int... groups) {
+        VerifyTools.requireNonNull(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = pattern;
+        this.namedGroups = null;
+        this.digitGroups = toIntegerList(groups);
+    }
+
+    public RegexpGroupExtractor(String pattern, String... groups) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = Pattern.compile(pattern);
+        this.digitGroups = null;
+        this.namedGroups = Arrays.asList(groups);
+    }
+
+    public RegexpGroupExtractor(Pattern pattern, String... groups) {
+        VerifyTools.requireNonNull(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = pattern;
+        this.digitGroups = null;
+        this.namedGroups = Arrays.asList(groups);
+    }
+
+    public RegexpGroupExtractor(String pattern, List<?> groups) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = Pattern.compile(pattern);
+
+        List<Integer> digitGroups = new ArrayList<>();
+        List<String> namedGroups = new ArrayList<>();
+        parseGroups(groups, digitGroups, namedGroups);
+        this.digitGroups = digitGroups.isEmpty() ? null : digitGroups;
+        this.namedGroups = namedGroups.isEmpty() ? null : namedGroups;
+    }
+
+    public RegexpGroupExtractor(Pattern pattern, List<?> groups) {
+        VerifyTools.requireNonNull(pattern, "pattern");
+        VerifyTools.requireNotBlank(groups, "groups");
+        this.pattern = pattern;
+        List<Integer> digitGroups = new ArrayList<>();
+        List<String> namedGroups = new ArrayList<>();
+        parseGroups(groups, digitGroups, namedGroups);
+        this.digitGroups = digitGroups.isEmpty() ? null : digitGroups;
+        this.namedGroups = namedGroups.isEmpty() ? null : namedGroups;
+    }
+
+    /** 提取规则 **/
+    public Pattern getPattern() {
+        return pattern;
+    }
+
+    /** 提取规则 **/
+    public void setPattern(Pattern pattern) {
+        this.pattern = pattern;
+    }
+
+    /** 提取规则 **/
+    public void setPattern(String pattern) {
+        this.pattern = Pattern.compile(pattern);
+    }
+
+    /** 排除规则 **/
+    public StringMatcher getExclude() {
+        return exclude;
+    }
+
+    /** 排除规则 **/
+    public void setExclude(StringMatcher exclude) {
+        this.exclude = exclude;
+    }
+
+    /** 提取组序号 **/
+    public List<Integer> getDigitGroups() {
+        return digitGroups;
+    }
+
+    /** 提取组序号 **/
+    public void setDigitGroups(List<Integer> digitGroups) {
+        this.digitGroups = digitGroups;
+    }
+
+    /** 提取组名称 **/
+    public List<String> getNamedGroups() {
+        return namedGroups;
+    }
+
+    /** 提取组名称 **/
+    public void setNamedGroups(List<String> namedGroups) {
+        this.namedGroups = namedGroups;
+    }
+
+    @Override
+    public GroupSubString extractFirst(String string) {
+        Matcher matcher = pattern.matcher(string);
+        if (matcher.find()) {
+            if (exclude == null || !exclude.matches(matcher.group())) {
+                GroupSubString result = newGroupSubString(matcher);
+                result.putExtra(this.getExtra());
+                return result;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public GroupSubStrings extractAll(String string) {
+        GroupSubStrings result = new GroupSubStrings();
+        Matcher matcher = pattern.matcher(string);
+        while (matcher.find()) {
+            if (exclude == null || !exclude.matches(matcher.group())) {
+                result.add(newGroupSubString(matcher));
+            }
+        }
+        if (result.isEmpty()) {
+            return null;
+        }
+        result.putExtra(this.getExtra());
+        return result;
+    }
+
+    private GroupSubString newGroupSubString(Matcher matcher) {
+        GroupSubString result = new GroupSubString();
+        result.setMatchContent(matcher.group());
+        result.setMatchStartIndex(matcher.start());
+        result.setMatchEndIndex(matcher.end());
+        if (digitGroups != null) {
+            for (int group : digitGroups) {
+                String content = matcher.group(group);
+                int start = matcher.start(group);
+                int end = matcher.end(group);
+                result.put(String.valueOf(group), new SubString(content, start, end));
+            }
+        }
+        if (namedGroups != null) {
+            for (String group : namedGroups) {
+                String content = matcher.group(group);
+                int start = matcher.start(group);
+                int end = matcher.end(group);
+                result.put(group, new SubString(content, start, end));
+            }
+        }
+        return result;
+    }
+
+    private static List<Integer> toIntegerList(int... numbers) {
+        List<Integer> list = new ArrayList<>();
+        for (int number : numbers) {
+            list.add(number);
+        }
+        return list;
+    }
+
+    private static void parseGroups(List<?> groups, List<Integer> digitGroups, List<String> namedGroups) {
+        for (int i = 0; i < groups.size(); i++) {
+            Object group = groups.get(i);
+            if (group instanceof Integer) {
+                digitGroups.add((Integer) group);
+            } else if (group instanceof String) {
+                namedGroups.add((String) group);
+            } else {
+                String type = group == null ? "null" : group.getClass().getSimpleName();
+                String msg = "Group must be integer or string, but type of groups[" + i + "] is " + type;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
+            }
+        }
+    }
+
+    private static final Parsers PARSER = new Parsers();
+
+    /**
+     * 根据字符串参数解析成RegexpGroupExtractor对象
+     *
+     * @param rule 规则字符串, 格式为: [1,2] regexp
+     * @return RegexpGroupExtractor对象
+     * @see Parsers#parseExtractor(String, Map)
+     * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+     */
+    public static RegexpGroupExtractor parseExtractor(String rule) {
+        return PARSER.parseExtractor(rule, null);
+    }
+
+    /**
+     * 根据字符串参数解析成RegexpGroupExtractor对象
+     *
+     * @param rule 规则字符串, 格式为: [1,2] regexp
+     * @param extras 附加参数
+     * @return RegexpGroupExtractor对象
+     * @see Parsers#parseExtractor(String, Map)
+     * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+     */
+    public static RegexpGroupExtractor parseExtractor(String rule, Map<String, Object> extras) {
+        return PARSER.parseExtractor(rule, extras);
+    }
+
+    /**
+     * 根据字符串参数解析成GroupExtractors对象
+     *
+     * @param rules 规则字符串数组
+     * @return GroupExtractors对象
+     * @see Parsers#parseExtractors(List)
+     * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+     */
+    public static GroupExtractors parseExtractors(List<String> rules) {
+        return PARSER.parseExtractors(rules);
+    }
+
+    public static class Parsers {
+
+        /**
+         * 根据字符串参数解析成RegexpGroupExtractor对象
+         *
+         * @param rule 规则字符串, 格式为: [1,2] regexp
+         * @param extras 附加参数
+         * @return RegexpGroupExtractor对象
+         * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+         */
+        public RegexpGroupExtractor parseExtractor(String rule, Map<String, Object> extras) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            StringAccess sa = new StringAccess(rule);
+            sa.skipWhitespace();
+            String g = sa.readInBracket('[', ']');
+            if (!sa.isLastReadSuccess()) {
+                String msg = "Group format error:  " + rule + "  , The correct format is [1,2] regexp";
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
+            }
+            g = g.trim();
+            if (g.length() == 0) {
+                String msg = "Group must must not be empty, --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
+            }
+            List<Object> groups = new ArrayList<>();
+            for (String group : StringTools.splits(g, ',')) {
+                if (StringTools.isDigit(group)) {
+                    groups.add(Integer.parseInt(group));
+                } else if (group.indexOf('$') < 0 && StringTools.isWordString(group)) {
+                    groups.add(group);
+                } else {
+                    String msg = "Group must be digit or word characters: " + g + " --> " + rule;
+                    throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
+                }
+            }
+            sa.skipWhitespace();
+            String regexp = sa.readToEnd();
+            try {
+                Pattern pattern = Pattern.compile(regexp);
+                RegexpGroupExtractor extractor = new RegexpGroupExtractor(pattern, groups);
+                if (extras != null) {
+                    extractor.putExtra(extras);
+                }
+                return extractor;
+            } catch (PatternSyntaxException e) {
+                String msg = "Regexp format error, --> " + regexp;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, e).setDetails(msg);
+            }
+        }
+
+        /**
+         * 根据字符串参数解析成GroupExtractors对象<br>
+         * 参数格式为: [1,2] regexp<br>
+         * 如果配置带额外属性的正则, 其格式为:
+         * <pre>
+         * |[1,2] regexp
+         * |    extraKey1 = extraValue1
+         * |    extraKey2 = extraValue2
+         * |    exclude = 排除规则
+         * </pre>
+         *
+         * @param rules 规则字符串数组
+         * @return GroupExtractors对象
+         * @throws ServiceException PARAMETER_FORMAT_ERROR, 参数格式错误
+         */
+        public GroupExtractors parseExtractors(List<String> rules) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            List<RegexpGroupExtractor> extractors = new ArrayList<>();
+            List<String> cleared = RuleTools.clearCommentLines(rules);
+            for (int i = 0, z = cleared.size(); i < z; i++) {
+                String string = cleared.get(i);
+                String trimmed = string.trim();
+                if (extractors.isEmpty()) {
+                    // 第1个正则之前的行, 判断是否缩进
+                    int indent = IndentTools.countLeadingIndentSize(string);
+                    if (indent > 0) {
+                        // 第1个正则之前的带缩进的行, 跳过
+                        continue;
+                    }
+                }
+                // 解析附加属性
+                List<String> extraLines = new ArrayList<>();
+                i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
+                Map<String, Object> extras = null;
+                if (!extraLines.isEmpty()) {
+                    extras = RuleTools.splitRuleKeyValues(extraLines).asJson();
+                }
+                // 解析正则表达式提取规则
+                List<RegexpGroupExtractor> list = doParseExtractors(trimmed, extras);
+                if (list != null) {
+                    for (RegexpGroupExtractor item : list) {
+                        doHandleExcludeMatcher(item);
+                        extractors.add(item);
+                    }
+                }
+            }
+            return new GroupExtractors(extractors);
+        }
+
+        protected List<RegexpGroupExtractor> doParseExtractors(String rule, Map<String, Object> extras) {
+            // 解析正则表达式提取规则
+            RegexpGroupExtractor extractor = parseExtractor(rule, extras);
+            doHandleExcludeMatcher(extractor);
+            return Collections.singletonList(extractor);
+        }
+
+        protected void doHandleExcludeMatcher(RegexpGroupExtractor extractor) {
+            if (extractor.containsExtra("exclude")) {
+                Object exclude = extractor.getExtra("exclude");
+                if (exclude instanceof String) {
+                    extractor.setExclude(doParseExcludeMatcher((String) exclude));
+                } else if (exclude instanceof StringMatcher) {
+                    extractor.setExclude((StringMatcher) exclude);
+                }
+            }
+        }
+
+        protected StringMatcher doParseExcludeMatcher(String exclude) {
+            return WrapStringMatcher.parseMatcher(exclude, "regexp");
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
new file mode 100644
index 0000000000000000000000000000000000000000..1247eaed9da3e4cd744f1554b13102157991d08f
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringExtractor.java
@@ -0,0 +1,324 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.model.reusable.ExtraData;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.IndentTools;
+import com.gitee.qdbp.tools.utils.RuleTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * 正则表达式字符串提取实现类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class RegexpStringExtractor extends ExtraData implements StringExtractor {
+
+    private static final long serialVersionUID = 6340075034069358703L;
+    /** 提取规则 **/
+    private Pattern pattern;
+    /** 排除规则 **/
+    private StringMatcher exclude;
+    /** 提取组序号 **/
+    private Integer digitGroup;
+    /** 提取组名称 **/
+    private String namedGroup;
+
+    public RegexpStringExtractor() {
+    }
+
+    public RegexpStringExtractor(String pattern, int group) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        this.pattern = Pattern.compile(pattern);
+        this.exclude = null;
+        this.digitGroup = group;
+        this.namedGroup = null;
+    }
+
+    public RegexpStringExtractor(Pattern pattern, int group) {
+        VerifyTools.requireNonNull(pattern, "pattern");
+        this.pattern = pattern;
+        this.exclude = null;
+        this.digitGroup = group;
+        this.namedGroup = null;
+    }
+
+    public RegexpStringExtractor(String pattern, String group) {
+        VerifyTools.requireNotBlank(pattern, "pattern");
+        this.pattern = Pattern.compile(pattern);
+        this.exclude = null;
+        this.digitGroup = null;
+        this.namedGroup = group;
+    }
+
+    public RegexpStringExtractor(Pattern pattern, String group) {
+        VerifyTools.requireNonNull(pattern, "pattern");
+        this.pattern = pattern;
+        this.exclude = null;
+        this.digitGroup = null;
+        this.namedGroup = group;
+    }
+
+    /** 提取规则 **/
+    public Pattern getPattern() {
+        return pattern;
+    }
+
+    /** 提取规则 **/
+    public void setPattern(Pattern pattern) {
+        this.pattern = pattern;
+    }
+
+    /** 提取规则 **/
+    public void setPattern(String pattern) {
+        this.pattern = Pattern.compile(pattern);
+    }
+
+    /** 排除规则 **/
+    public StringMatcher getExclude() {
+        return exclude;
+    }
+
+    /** 排除规则 **/
+    public void setExclude(StringMatcher exclude) {
+        this.exclude = exclude;
+    }
+
+    /** 提取组序号 **/
+    public Integer getDigitGroup() {
+        return digitGroup;
+    }
+
+    /** 提取组序号 **/
+    public void setDigitGroup(Integer digitGroup) {
+        this.digitGroup = digitGroup;
+    }
+
+    /** 提取组名称 **/
+    public String getNamedGroup() {
+        return namedGroup;
+    }
+
+    /** 提取组名称 **/
+    public void setNamedGroup(String namedGroup) {
+        this.namedGroup = namedGroup;
+    }
+
+    @Override
+    public LocatedSubString extractFirst(String string) {
+        Matcher matcher = pattern.matcher(string);
+        if (matcher.find()) {
+            LocatedSubString result = newLocationString(matcher);
+            if (exclude == null || !exclude.matches(matcher.group())) {
+                result.putExtra(this.getExtra());
+                return result;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public LocatedSubStrings extractAll(String string) {
+        LocatedSubStrings result = new LocatedSubStrings();
+        Matcher matcher = pattern.matcher(string);
+        while (matcher.find()) {
+            if (exclude == null || !exclude.matches(matcher.group())) {
+                result.add(newLocationString(matcher));
+            }
+        }
+        if (result.isEmpty()) {
+            return null;
+        }
+        result.putExtra(this.getExtra());
+        return result;
+    }
+
+    private LocatedSubString newLocationString(Matcher matcher) {
+        LocatedSubString result = new LocatedSubString();
+        result.setMatchContent(matcher.group());
+        result.setMatchStartIndex(matcher.start());
+        result.setMatchEndIndex(matcher.end());
+        if (this.digitGroup != null) {
+            result.setGroupContent(matcher.group(this.digitGroup));
+            result.setGroupStartIndex(matcher.start(this.digitGroup));
+            result.setGroupEndIndex(matcher.end(this.digitGroup));
+        } else {
+            result.setGroupContent(matcher.group(this.namedGroup));
+            result.setGroupStartIndex(matcher.start(this.namedGroup));
+            result.setGroupEndIndex(matcher.end(this.namedGroup));
+        }
+        return result;
+    }
+
+    private static final Parsers PARSER = new Parsers();
+
+    /**
+     * 根据字符串参数解析成RegexpGroupExtractor对象
+     *
+     * @param rule 规则字符串, 格式为: [1] regexp
+     * @see Parsers#parseExtractor(String, Map)
+     * @return RegexpGroupExtractor对象
+     */
+    public RegexpStringExtractor parseExtractor(String rule) {
+        return PARSER.parseExtractor(rule, null);
+    }
+
+    /**
+     * 根据字符串参数解析成RegexpGroupExtractor对象
+     *
+     * @param rule 规则字符串, 格式为: [1] regexp
+     * @param extras 附加参数
+     * @see Parsers#parseExtractor(String, Map)
+     * @return RegexpGroupExtractor对象
+     */
+    public RegexpStringExtractor parseExtractor(String rule, Map<String, Object> extras) {
+        return PARSER.parseExtractor(rule, extras);
+    }
+
+    /**
+     * 根据字符串参数解析成StringExtractors对象
+     *
+     * @param rules 字符串数组
+     * @see Parsers#parseExtractors(List)
+     * @return StringExtractors对象
+     */
+    public static StringExtractors parseExtractors(List<String> rules) {
+        return PARSER.parseExtractors(rules);
+    }
+
+    public static class Parsers {
+
+        /**
+         * 根据字符串参数解析成RegexpGroupExtractor对象
+         *
+         * @param rule 规则字符串, 格式为: [1] regexp
+         * @param extras 附加参数
+         * @return RegexpGroupExtractor对象
+         */
+        public RegexpStringExtractor parseExtractor(String rule, Map<String, Object> extras) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            StringAccess sa = new StringAccess(rule);
+            sa.skipWhitespace();
+            String group = sa.readInBracket('[', ']');
+            if (!sa.isLastReadSuccess()) {
+                String msg = "Group format error, The correct format is [1] regexp, --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
+            }
+            group = group.trim();
+            if (group.length() == 0) {
+                String msg = "Group must must not be empty, --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
+            }
+            Integer digitGroup = null;
+            String namedGroup = null;
+            if (StringTools.isDigit(group)) {
+                digitGroup = Integer.parseInt(group);
+            } else if (group.indexOf('$') < 0 && StringTools.isWordString(group)) {
+                namedGroup = group;
+            } else {
+                String msg = "Group must be digit or word characters: " + group + " --> " + rule;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR).setDetails(msg);
+            }
+            sa.skipWhitespace();
+            String regexp = sa.readToEnd();
+            Pattern pattern;
+            try {
+                pattern = Pattern.compile(regexp);
+            } catch (PatternSyntaxException e) {
+                String msg = "Regexp format error, --> " + regexp;
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR, e).setDetails(msg);
+            }
+            RegexpStringExtractor extractor;
+            if (digitGroup != null) {
+                extractor = new RegexpStringExtractor(pattern, digitGroup);
+            } else {
+                extractor = new RegexpStringExtractor(pattern, namedGroup);
+            }
+            if (extras != null) {
+                extractor.putExtra(extras);
+            }
+            return extractor;
+        }
+
+        /**
+         * 根据字符串参数解析成StringExtractors对象<br>
+         * 参数格式为: [1] regexp<br>
+         * 如果配置带额外属性的正则, 其格式为:
+         * <pre>
+         * |[1] regexp
+         * |    extraKey1 = extraValue1
+         * |    extraKey2 = extraValue2
+         * |    exclude = 排除规则
+         * </pre>
+         *
+         * @param rules 字符串数组
+         * @return StringExtractors对象
+         */
+        public StringExtractors parseExtractors(List<String> rules) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            List<RegexpStringExtractor> extractors = new ArrayList<>();
+            List<String> cleared = RuleTools.clearCommentLines(rules);
+            for (int i = 0, z = cleared.size(); i < z; i++) {
+                String string = cleared.get(i);
+                String trimmed = string.trim();
+                if (extractors.isEmpty()) {
+                    // 第1个正则之前的行, 判断是否缩进
+                    int indent = IndentTools.countLeadingIndentSize(string);
+                    if (indent > 0) {
+                        // 第1个正则之前的带缩进的行, 跳过
+                        continue;
+                    }
+                }
+                // 解析附加属性
+                List<String> extraLines = new ArrayList<>();
+                i += RuleTools.collectNextIndentLines(cleared, i + 1, extraLines);
+                Map<String, Object> extras = null;
+                if (!extraLines.isEmpty()) {
+                    List<KeyString> keyStrings = RuleTools.splitRuleKeyValues(extraLines);
+                    extras = KeyString.toMap(keyStrings);
+                }
+                // 解析正则表达式提取规则
+                List<RegexpStringExtractor> list = doParseExtractors(trimmed, extras);
+                if (list != null) {
+                    for (RegexpStringExtractor item : list) {
+                        doHandleExcludeMatcher(item);
+                        extractors.add(item);
+                    }
+                }
+            }
+            return new StringExtractors(extractors);
+        }
+
+        protected List<RegexpStringExtractor> doParseExtractors(String rule, Map<String, Object> extras) {
+            // 解析正则表达式提取规则
+            RegexpStringExtractor extractor = parseExtractor(rule, extras);
+            return Collections.singletonList(extractor);
+        }
+
+        protected StringMatcher doParseExcludeMatcher(String exclude) {
+            return WrapStringMatcher.parseMatcher(exclude, "regexp");
+        }
+
+        protected void doHandleExcludeMatcher(RegexpStringExtractor extractor) {
+            if (extractor.containsExtra("exclude")) {
+                Object exclude = extractor.getExtra("exclude");
+                if (exclude instanceof String) {
+                    extractor.setExclude(doParseExcludeMatcher((String) exclude));
+                } else if (exclude instanceof StringMatcher) {
+                    extractor.setExclude((StringMatcher) exclude);
+                }
+            }
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringReplacer.java
index 5eb510ab392dddc87ed30c6e33b67d84f3dba903..e86b70f8b4380ae8b01ac7e78f62d8246d85fc83 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/RegexpStringReplacer.java
@@ -1,6 +1,10 @@
 package com.gitee.qdbp.able.matches;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.regex.Pattern;
+import com.gitee.qdbp.tools.utils.RuleTools;
+import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -18,7 +22,7 @@ public class RegexpStringReplacer implements StringReplacer {
 
     /**
      * 构造函数
-     * 
+     *
      * @param pattern 正则表达式
      * @param replacement 替换内容
      */
@@ -31,7 +35,7 @@ public class RegexpStringReplacer implements StringReplacer {
 
     /**
      * 构造函数
-     * 
+     *
      * @param pattern 正则表达式
      * @param replacement 替换内容
      */
@@ -55,7 +59,82 @@ public class RegexpStringReplacer implements StringReplacer {
 
     @Override
     public String toString() {
-        return "regexp:" + pattern + " --> " + replacement;
+        return "regexp:" + pattern + " -- " + replacement;
     }
 
+    /**
+     * 解析文本替换规则<br>
+     * // 年初现金替换为期初现金<br>
+     * 年([初末])现金 -- 期$1现金<br>
+     * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+     * (?<=所)支付的现金 -- 所支付的现金<br>
+     *
+     * @param pattern 规则内容
+     * @return 规则对象
+     */
+    public static RegexpStringReplacer parseReplacer(String pattern) {
+        return parseReplacer(pattern, " -- ");
+    }
+
+    /**
+     * 解析文本替换规则<br>
+     * // 年初现金替换为期初现金<br>
+     * 年([初末])现金 -- 期$1现金<br>
+     * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+     * (?<=所)支付的现金 -- 所支付的现金<br>
+     *
+     * @param pattern 规则内容
+     * @param delimiter 分隔符
+     * @return 规则对象
+     */
+    public static RegexpStringReplacer parseReplacer(String pattern, String delimiter) {
+        int splitIndex = pattern.indexOf(delimiter);
+        if (splitIndex < 0) {
+            return null;
+        }
+        String source = StringTools.trimRight(pattern.substring(0, splitIndex));
+        String target = StringTools.trimLeft(pattern.substring(splitIndex + delimiter.length()));
+        if ("{null}".equals(target)) {
+            target = "";
+        }
+        return new RegexpStringReplacer(source, target);
+    }
+
+    /**
+     * 解析文本替换规则<br>
+     * // 年初现金替换为期初现金<br>
+     * 年([初末])现金 -- 期$1现金<br>
+     * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+     * (?<=所)支付的现金 -- 所支付的现金<br>
+     *
+     * @param patterns 规则内容
+     * @return 规则对象
+     */
+    public static WrapStringReplacer parseReplacers(List<String> patterns) {
+        return parseReplacers(patterns, " -- ");
+    }
+
+    /**
+     * 解析文本替换规则<br>
+     * // 年初现金替换为期初现金<br>
+     * 年([初末])现金 -- 期$1现金<br>
+     * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+     * (?<=所)支付的现金 -- 所支付的现金<br>
+     *
+     * @param rules 规则内容
+     * @param delimiter 分隔符
+     * @return 规则对象
+     */
+    public static WrapStringReplacer parseReplacers(List<String> rules, String delimiter) {
+        List<StringReplacer> replacers = new ArrayList<>();
+        List<String> cleared = RuleTools.clearCommentLines(rules);
+        for (String rule : cleared) {
+            String trimmed = rule.trim();
+            RegexpStringReplacer replacer = parseReplacer(trimmed, delimiter);
+            if (replacer != null) {
+                replacers.add(replacer);
+            }
+        }
+        return new WrapStringReplacer(replacers);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/SimpleStringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/SimpleStringReplacer.java
index 3be66ea681d37925b61a7d2326f22e829c89853c..538cb28240c9f0234aa5ba91ab826c8fc4cb98af 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/SimpleStringReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/SimpleStringReplacer.java
@@ -20,4 +20,37 @@ public class SimpleStringReplacer implements StringReplacer {
     public String toString() {
         return "simple:" + ConvertTools.joinToString(rules, '|');
     }
+
+
+    /**
+     * 解析文本替换规则<br>
+     * 格式: AA -- BB<br>
+     *
+     * @param pattern 规则内容
+     * @return 规则对象
+     */
+    public static StringReplacer parseReplacer(String pattern) {
+        return parseReplacer(pattern, " -- ");
+    }
+
+    /**
+     * 解析文本替换规则<br>
+     * 格式: AA -- BB<br>
+     *
+     * @param pattern 规则内容
+     * @param delimiter 分隔符
+     * @return 规则对象
+     */
+    public static StringReplacer parseReplacer(String pattern, String delimiter) {
+        int splitIndex = pattern.indexOf(delimiter);
+        if (splitIndex < 0) {
+            return null;
+        }
+        String source = StringTools.trimRight(pattern.substring(0, splitIndex));
+        String target = StringTools.trimLeft(pattern.substring(splitIndex + delimiter.length()));
+        if ("{null}".equals(target)) {
+            target = "";
+        }
+        return new SimpleStringReplacer(source, target);
+    }
 }
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StartsFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/StartsFileMatcher.java
index 52cb3a4b2729a848d070b595dc5d032e6bb8e35b..98823d41bc442b06b244344f0a2ffe4334d0da24 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StartsFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StartsFileMatcher.java
@@ -32,4 +32,19 @@ public class StartsFileMatcher extends BaseFileMatcher {
         super(new StartsStringMatcher(pattern, mode), target);
     }
 
+    public static StartsFileMatcher ofNameMatches(String pattern) {
+        return new StartsFileMatcher(pattern, Target.FileName, Matches.Positive);
+    }
+
+    public static StartsFileMatcher ofPathMatches(String pattern) {
+        return new StartsFileMatcher(pattern, Target.FilePath, Matches.Positive);
+    }
+
+    public static StartsFileMatcher ofNameNotMatches(String pattern) {
+        return new StartsFileMatcher(pattern, Target.FileName, Matches.Negative);
+    }
+
+    public static StartsFileMatcher ofPathNotMatches(String pattern) {
+        return new StartsFileMatcher(pattern, Target.FilePath, Matches.Negative);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb261848d61ac65c30466ceaa81970a979d6e209
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractor.java
@@ -0,0 +1,26 @@
+package com.gitee.qdbp.able.matches;
+
+/**
+ * 字符串提取接口
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public interface StringExtractor {
+
+    /**
+     * 提取第1个匹配项
+     *
+     * @param string 原文本
+     * @return 截取到的文本以及在原文中的位置
+     */
+    LocatedSubString extractFirst(String string);
+
+    /**
+     * 提取所有匹配项
+     *
+     * @param string 原文本
+     * @return 截取到的文本以及在原文中的位置, 通过get(1)方式取指定组的匹配值
+     */
+    LocatedSubStrings extractAll(String string);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
new file mode 100644
index 0000000000000000000000000000000000000000..98c25361f572a69ab57006301edbd99fe37791e4
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringExtractors.java
@@ -0,0 +1,57 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.List;
+
+/**
+ * 字符串提取集合实现类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+public class StringExtractors implements StringExtractor {
+    private final List<? extends StringExtractor> extractors;
+
+    public <T extends StringExtractor> StringExtractors(List<T> extractors) {
+        this.extractors = extractors;
+    }
+
+    public List<? extends StringExtractor> getExtractors() {
+        return extractors;
+    }
+
+    @Override
+    public LocatedSubString extractFirst(String string) {
+        for (int i = 0, z = extractors.size(); i < z; i++) {
+            StringExtractor extractor = extractors.get(i);
+            LocatedSubString result = extractor.extractFirst(string);
+            if (result != null) {
+                if (!result.containsExtra("index")) {
+                    result.putExtra("index", i);
+                }
+                if (!result.containsExtra("rule")) {
+                    result.putExtra("rule", extractor);
+                }
+                return result;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public LocatedSubStrings extractAll(String string) {
+        for (int i = 0, z = extractors.size(); i < z; i++) {
+            StringExtractor extractor = extractors.get(i);
+            LocatedSubStrings result = extractor.extractAll(string);
+            if (result != null) {
+                if (!result.containsExtra("index")) {
+                    result.putExtra("index", i);
+                }
+                if (!result.containsExtra("rule")) {
+                    result.putExtra("rule", extractor);
+                }
+                return result;
+            }
+        }
+        return null;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
index 7fcdfc53bb9ffe67172e4c8f796b1d79043bcbf5..d8ec4fc0c0f6b88d53c4be87dc86c769f75ab560 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringMatcher.java
@@ -29,6 +29,16 @@ package com.gitee.qdbp.able.matches;
  */
 public interface StringMatcher {
 
+    /**
+     * Returns <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
+     * <code>false</code> otherwise.
+     *
+     * @param source the source to match
+     * @return <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
+     * <code>false</code> otherwise.
+     */
+    boolean matches(String source);
+
     /** 匹配模式 **/
     enum Matches {
         /** 肯定模式, 符合条件为匹配 **/
@@ -36,19 +46,29 @@ public interface StringMatcher {
         /** 否定模式, 不符合条件为匹配 **/
         Negative
     }
-    
+
     /** 逻辑类型: 多个匹配规则使用and还是or关联 **/
     enum LogicType {
         AND, OR
     }
 
-    /**
-     * Returns <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
-     * <code>false</code> otherwise.
-     *
-     * @param source  the source to match
-     * @return <code>true</code> if the given <code>source</code> matches the specified <code>pattern</code>,
-     *         <code>false</code> otherwise.
-     */
-    boolean matches(String source);
+    /** 基础的规则解析处理接口 **/
+    interface ParserByBase {
+
+        /** 是否支持解析 **/
+        boolean supports(String rule);
+
+        /** 解析匹配规则 **/
+        StringMatcher parse(String rule, String defaultMode);
+    }
+
+    /** 按匹配模式解析的规则解析处理接口 **/
+    interface ParserByMode {
+
+        /** 支持的匹配模式 **/
+        String[] supportModes();
+
+        /** 解析匹配规则 **/
+        StringMatcher parse(String rule, String mode, String defaultMode, Matches matchType);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/StringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/StringReplacer.java
index e909124bc8a5cc35a5555f6df2c515cc392cb122..1140724df21dcee09534e7a1f4bc9c6a2e643d91 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/StringReplacer.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/StringReplacer.java
@@ -10,9 +10,29 @@ public interface StringReplacer {
 
     /**
      * 替换字符串
-     * 
+     *
      * @param string 待替换的源字符串
      * @return 替换后的字符串
      */
     String replace(String string);
+
+    /** 基础的规则解析处理接口 **/
+    interface ParserByBase {
+
+        /** 是否支持解析 **/
+        boolean supports(String rule);
+
+        /** 解析匹配规则 **/
+        StringReplacer parse(String rule, String defaultMode);
+    }
+
+    /** 按mode解析的规则解析处理接口 **/
+    interface ParserByMode {
+
+        /** 支持的匹配模式 **/
+        String[] supportModes();
+
+        /** 解析匹配规则 **/
+        StringReplacer parse(String rule, String mode, String defaultMode);
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
index ce572f951d57ef4f7e010c8e28af16ee2f2d8b42..e32352a659da7c067f2c6a16335ee6588deec134 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapFileMatcher.java
@@ -5,6 +5,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import com.gitee.qdbp.able.matches.StringMatcher.LogicType;
+import com.gitee.qdbp.able.matches.StringMatcher.Matches;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
@@ -78,7 +79,7 @@ public class WrapFileMatcher implements FileMatcher {
     @Override
     public String toString() {
         if (this.matchers == null || this.matchers.isEmpty()) {
-            return "NULL";
+            return "{null}";
         }
         StringBuilder buffer = new StringBuilder();
         String logicType = this.logicType == null ? "OR" : this.logicType.name();
@@ -139,16 +140,16 @@ public class WrapFileMatcher implements FileMatcher {
 
     /**
      * 解析FileMatcher规则<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的解析为EqualsStringMatcher<br>
+     * <br>
+     * 例如: name:ant:*.txt, 表示用fileName匹配ant规则*.txt<br>
+     * 例如: path:contains!:bak, 表示filePath不含有bak的<br>
+     * <br>
+     * 第1段, name或path表示匹配目标, 是根据文件名还是文件路径进行比对<br>
+     * 第2段, 匹配规则, 详见 StringMatcher<br>
      * 
      * @param pattern 匹配规则
      * @return FileMatcher
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      */
     public static FileMatcher parseMatcher(String pattern) {
         VerifyTools.requireNotBlank(pattern, "pattern");
@@ -162,18 +163,12 @@ public class WrapFileMatcher implements FileMatcher {
      * 例如: path:contains!:bak, 表示filePath不含有bak的<br>
      * <br>
      * 第1段, name或path表示匹配目标, 是根据文件名还是文件路径进行比对<br>
-     * 第2段, 匹配规则:<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 严格模式下解析为EqualsStringMatcher, 否则解析为ContainsStringMatcher<br>
+     * 第2段, 匹配规则, 详见 StringMatcher<br>
      * 
      * @param pattern 匹配规则
      * @param strict 是否使用严格格式
      * @return FileMatcher
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      */
     public static FileMatcher parseMatcher(String pattern, boolean strict) {
         VerifyTools.requireNotBlank(pattern, "pattern");
@@ -188,24 +183,23 @@ public class WrapFileMatcher implements FileMatcher {
      * 例如: path:contains!:bak, 表示filePath不含有bak的<br>
      * <br>
      * 第1段, name或path表示匹配目标, 是根据文件名还是文件路径进行比对<br>
-     * 第2段, 匹配规则:<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 使用defaultMode指定的匹配方式<br>
-     * 
+     * 第2段, 匹配规则, 详见 StringMatcher<br>
+     *
      * @param pattern 匹配规则
      * @param defaultTarget 默认匹配目标
      * @param defaultMode 默认匹配方式
      * @return FileMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String, String)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      * @since 5.2.0
      */
     public static FileMatcher parseMatcher(String pattern, String defaultTarget, String defaultMode) {
         VerifyTools.requireNotBlank(pattern, "pattern");
+        if (pattern.startsWith("extension:")) {
+            return new ExtensionFileMatcher(pattern.substring("extension:".length()));
+        } else if (pattern.startsWith("extension!:")) {
+            return new ExtensionFileMatcher(pattern.substring("extension!:".length()), Matches.Negative);
+        }
         Target target;
         if (pattern.startsWith("name:")) {
             target = Target.FileName;
@@ -225,60 +219,58 @@ public class WrapFileMatcher implements FileMatcher {
     }
 
     /**
-     * 解析FileMatcher规则列表, 以逗号或换行符分隔<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的解析为EqualsStringMatcher<br>
+     * 解析FileMatcher规则列表, 以逗号或换行符分隔
      * 
-     * @param patterns 匹配规则列表
+     * @param rules 匹配规则列表
      * @param logicType 多个匹配规则使用and还是or关联
      * @return FileMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see WrapStringMatcher.Parsers#parseMatchers(String, LogicType)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static FileMatcher parseMatchers(String patterns, LogicType logicType) {
-        VerifyTools.requireNotBlank(patterns, "patterns");
-        return parseMatchers(patterns, logicType, "name", "equals", ',', '\n');
+    public static FileMatcher parseMatchers(String rules, LogicType logicType) {
+        VerifyTools.requireNotBlank(rules, "rules");
+        return parseMatchers(rules, logicType, "name", "equals", ',', '\n');
     }
 
     /**
-     * 解析FileMatcher规则列表<br>
-     * 如: parseMatcher(pattern, Logic.OR, "name", "ant", ',', '\n'); // 默认按文件名以ant规则匹配<br>
-     * <br>
-     * 例如: name:ant:*.txt, 表示用fileName匹配ant规则*.txt<br>
-     * 例如: path:contains!:bak, 表示filePath不含有bak的<br>
-     * <br>
-     * 第1段, name或path表示匹配目标, 是根据文件名还是文件路径进行比对<br>
-     * 第2段, 匹配规则:<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 使用defaultMode指定的匹配方式<br>
+     * 解析FileMatcher规则列表
      * 
-     * @param patterns 匹配规则列表
+     * @param rules 匹配规则列表
      * @param defaultTarget 默认匹配目标
      * @param defaultMode 默认匹配方式
      * @param chars 分隔符
      * @return FileMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see WrapStringMatcher.Parsers#parseMatchers(String, LogicType, String, char...)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static FileMatcher parseMatchers(String patterns, LogicType logicType, String defaultTarget,
+    public static FileMatcher parseMatchers(String rules, LogicType logicType, String defaultTarget,
             String defaultMode, char... chars) {
-        VerifyTools.requireNotBlank(patterns, "patterns");
+        VerifyTools.requireNotBlank(rules, "rules");
         if (chars == null || chars.length == 0) {
-            chars = new char[] { ',', '\n' };
+            chars = new char[] {',', '\n'};
         }
-        String[] array = StringTools.split(patterns, chars);
+        List<String> list = StringTools.splits(rules, chars);
+        return parseMatchers(list, logicType, defaultTarget, defaultMode);
+    }
+
+    /**
+     * 解析FileMatcher规则列表
+     *
+     * @param rules 匹配规则列表
+     * @param defaultTarget 默认匹配目标
+     * @param defaultMode 默认匹配方式
+     * @return FileMatcher
+     * @see WrapStringMatcher.Parsers#parseMatchers(List, LogicType, String)
+     * @see WrapStringMatcher.Parsers#parseMatcher(String)
+     * @since 5.4.10
+     */
+    public static FileMatcher parseMatchers(List<String> rules, LogicType logicType, String defaultTarget,
+            String defaultMode) {
+        VerifyTools.requireNotBlank(rules, "rules");
         List<FileMatcher> matchers = new ArrayList<>();
-        for (String pattern : array) {
+        for (String pattern : rules) {
             if (VerifyTools.isNotBlank(pattern)) {
                 matchers.add(parseMatcher(pattern, defaultTarget, defaultMode));
             }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
index 55b2c7556eb3cc3ea37cd6711b8029a230335cc8..550b0c82a88f14cd023bf4969a88210b0d83dca0 100644
--- a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringMatcher.java
@@ -2,8 +2,11 @@ package com.gitee.qdbp.able.matches;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
-import com.gitee.qdbp.tools.utils.ConvertTools;
+import java.util.Map;
+import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.RuleTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
@@ -15,37 +18,42 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  */
 public class WrapStringMatcher implements StringMatcher {
 
-    private List<StringMatcher> matchers;
     /** 多个匹配规则使用and还是or关联 **/
     private LogicType logicType;
+    private final List<StringMatcher> matchers = new ArrayList<>();
 
-    public WrapStringMatcher(String... patterns) {
+    public WrapStringMatcher(String... rules) {
         this.logicType = LogicType.AND;
-        this.addMatchers(patterns);
+        this.addMatchers(rules);
     }
 
-    public WrapStringMatcher(LogicType logicType, String... patterns) {
+    public WrapStringMatcher(LogicType logicType, String... rules) {
         this.logicType = logicType;
-        this.addMatchers(patterns);
+        this.addMatchers(rules);
     }
 
     public WrapStringMatcher(StringMatcher... matchers) {
         this.logicType = LogicType.AND;
-        this.addMatcher(matchers);
+        this.addMatchers(matchers);
     }
 
     public WrapStringMatcher(LogicType logicType, StringMatcher... matchers) {
         this.logicType = logicType;
-        this.addMatcher(matchers);
+        this.addMatchers(matchers);
+    }
+
+    public WrapStringMatcher(List<StringMatcher> matchers) {
+        this.logicType = LogicType.AND;
+        this.addMatchers(matchers);
+    }
+
+    public WrapStringMatcher(LogicType logicType, List<StringMatcher> matchers) {
+        this.logicType = logicType;
+        this.addMatchers(matchers);
     }
 
     @Override
     public boolean matches(String source) {
-        if (this.matchers == null) {
-            // matchers=null和matchers是空数组效果一致
-            return this.logicType == LogicType.AND;
-        }
-
         for (StringMatcher matcher : this.matchers) {
             boolean matches = matcher.matches(source);
             if (this.logicType == LogicType.AND) {
@@ -66,8 +74,8 @@ public class WrapStringMatcher implements StringMatcher {
 
     @Override
     public String toString() {
-        if (this.matchers == null || this.matchers.isEmpty()) {
-            return "NULL";
+        if (this.matchers.isEmpty()) {
+            return "{null}";
         }
         StringBuilder buffer = new StringBuilder();
         String logicType = this.logicType == null ? "OR" : this.logicType.name();
@@ -91,31 +99,33 @@ public class WrapStringMatcher implements StringMatcher {
 
     /** 设置匹配规则 **/
     public void setMatchers(List<StringMatcher> matchers) {
-        this.matchers = matchers;
+        this.matchers.clear();
+        this.addMatchers(matchers);
     }
 
     /** 增加匹配规则 **/
-    public void addMatchers(String... patterns) {
-        if (this.matchers == null) {
-            this.matchers = new ArrayList<>();
-        }
-        if (patterns != null && patterns.length > 0) {
-            for (String pattern : patterns) {
-                this.matchers.add(parseMatcher(pattern, true));
+    private void addMatchers(String... rules) {
+        if (rules != null && rules.length > 0) {
+            for (String rule : rules) {
+                this.matchers.add(parseMatcher(rule, true));
             }
         }
     }
 
     /** 增加匹配规则 **/
-    public void addMatcher(StringMatcher... matchers) {
-        if (this.matchers == null) {
-            this.matchers = new ArrayList<>();
-        }
+    public void addMatchers(StringMatcher... matchers) {
         if (matchers != null && matchers.length > 0) {
             Collections.addAll(this.matchers, matchers);
         }
     }
 
+    /** 增加匹配规则 **/
+    public void addMatchers(List<StringMatcher> matchers) {
+        if (matchers != null && matchers.size() > 0) {
+            this.matchers.addAll(matchers);
+        }
+    }
+
     /** 多个匹配规则使用and还是or关联 **/
     public LogicType getLogicType() {
         return logicType;
@@ -126,173 +136,411 @@ public class WrapStringMatcher implements StringMatcher {
         this.logicType = logicType;
     }
 
+    private static final Parsers MATCHER_PARSER = new Parsers();
+
     /**
-     * 解析StringMatcher规则<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的解析为EqualsStringMatcher<br>
-     * 
-     * @param pattern 匹配规则
+     * 解析StringMatcher规则
+     *
+     * @param rule 匹配规则
      * @return StringMatcher
+     * @see Parsers#parseMatcher(String)
      */
-    public static StringMatcher parseMatcher(String pattern) {
-        VerifyTools.requireNotBlank(pattern, "pattern");
-        return parseMatcher(pattern, "equals");
+    public static StringMatcher parseMatcher(String rule) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return parseMatcher(rule, "equals");
     }
 
     /**
-     * 解析StringMatcher规则<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 严格模式下解析为EqualsStringMatcher, 否则解析为ContainsStringMatcher<br>
-     * 
-     * @param pattern 匹配规则
+     * 解析StringMatcher规则
+     *
+     * @param rule 匹配规则
      * @param strict 是否使用严格格式
      * @return StringMatcher
+     * @see Parsers#parseMatcher(String, boolean)
+     * @see Parsers#parseMatcher(String)
      */
-    public static StringMatcher parseMatcher(String pattern, boolean strict) {
-        VerifyTools.requireNotBlank(pattern, "pattern");
-        return parseMatcher(pattern, strict ? "equals" : "contains");
+    public static StringMatcher parseMatcher(String rule, boolean strict) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return MATCHER_PARSER.parseMatcher(rule, strict);
     }
 
     /**
-     * 解析StringMatcher规则<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 使用defaultMode指定的匹配方式<br>
-     * 
-     * @param pattern 匹配规则
+     * 解析StringMatcher规则
+     *
+     * @param rule 匹配规则
      * @param defaultMode 默认匹配方式
      * @return StringMatcher
+     * @see Parsers#parseMatcher(String, String)
+     * @see Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static StringMatcher parseMatcher(String pattern, String defaultMode) {
-        VerifyTools.requireNotBlank(pattern, "pattern");
-        if (pattern.startsWith("regexp:")) {
-            String value = StringTools.removePrefix(pattern, "regexp:");
-            return new RegexpStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("regexp!:")) {
-            String value = StringTools.removePrefix(pattern, "regexp!:");
-            return new RegexpStringMatcher(value, Matches.Negative);
-        } else if (pattern.startsWith("ant:")) {
-            String value = StringTools.removePrefix(pattern, "ant:");
-            return new AntStringMatcher(value, true, Matches.Positive);
-        } else if (pattern.startsWith("ant!:")) {
-            String value = StringTools.removePrefix(pattern, "ant!:");
-            return new AntStringMatcher(value, true, Matches.Negative);
-        } else if (pattern.startsWith("equals:")) {
-            String value = StringTools.removePrefix(pattern, "equals:");
-            return new EqualsStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("equals!:")) {
-            String value = StringTools.removePrefix(pattern, "equals!:");
-            return new EqualsStringMatcher(value, Matches.Negative);
-        } else if (pattern.startsWith("contains:")) {
-            String value = StringTools.removePrefix(pattern, "contains:");
-            return new ContainsStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("contains!:")) {
-            String value = StringTools.removePrefix(pattern, "contains!:");
-            return new ContainsStringMatcher(value, Matches.Negative);
-        } else if (pattern.startsWith("starts:")) {
-            String value = StringTools.removePrefix(pattern, "starts:");
-            return new StartsStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("starts!:")) {
-            String value = StringTools.removePrefix(pattern, "starts!:");
-            return new StartsStringMatcher(value, Matches.Negative);
-        } else if (pattern.startsWith("ends:")) {
-            String value = StringTools.removePrefix(pattern, "ends:");
-            return new EndsStringMatcher(value, Matches.Positive);
-        } else if (pattern.startsWith("ends!:")) {
-            String value = StringTools.removePrefix(pattern, "contains!:");
-            return new EndsStringMatcher(value, Matches.Negative);
-        } else {
-            if ("ant".equals(defaultMode)) {
-                return new AntStringMatcher(pattern, true, Matches.Positive);
-            } else if ("regexp".equals(defaultMode)) {
-                return new RegexpStringMatcher(pattern, Matches.Positive);
-            } else if ("equals".equals(defaultMode)) {
-                return new EqualsStringMatcher(pattern, Matches.Positive);
-            } else if ("contains".equals(defaultMode)) {
-                return new ContainsStringMatcher(pattern, Matches.Positive);
-            } else if ("starts".equals(defaultMode)) {
-                return new StartsStringMatcher(pattern, Matches.Positive);
-            } else if ("ends".equals(defaultMode)) {
-                return new EndsStringMatcher(pattern, Matches.Positive);
-            } else {
-                return new EqualsStringMatcher(pattern, Matches.Positive);
-            }
-        }
+    public static StringMatcher parseMatcher(String rule, String defaultMode) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return MATCHER_PARSER.parseMatcher(rule, defaultMode);
     }
 
     /**
-     * 解析StringMatcher规则列表, 以逗号或换行符分隔<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的解析为EqualsStringMatcher<br>
-     * 
-     * @param patterns 匹配规则列表
+     * 解析StringMatcher规则列表, 以逗号或换行符分隔
+     *
+     * @param rules 匹配规则列表
      * @param logicType 多个匹配规则使用and还是or关联
      * @return StringMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see Parsers#parseMatchers(String, LogicType)
+     * @see Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static StringMatcher parseMatchers(String patterns, LogicType logicType) {
-        VerifyTools.requireNotBlank(patterns, "patterns");
-        return parseMatchers(patterns, logicType, "equals", ',', '\n');
+    public static StringMatcher parseMatchers(String rules, LogicType logicType) {
+        VerifyTools.requireNotBlank(rules, "rules");
+        return MATCHER_PARSER.parseMatchers(rules, logicType);
     }
 
     /**
-     * 解析StringMatcher规则列表<br>
-     * 如: parseMatchers(pattern, Logic.OR, "ant", ',', '\n'); // 默认以ant规则匹配<br>
-     * regexp:开头的解析为RegexpStringMatcher<br>
-     * ant:开头的解析为AntStringMatcher<br>
-     * equals:开头的解析为EqualsStringMatcher<br>
-     * contains:开头的解析为ContainsStringMatcher<br>
-     * starts:开头的解析为StartsStringMatcher<br>
-     * ends:开头的解析为EndsStringMatcher<br>
-     * 其余的, 使用defaultMode指定的匹配方式<br>
-     * 
-     * @param patterns 匹配规则列表
+     * 解析StringMatcher规则列表
+     *
+     * @param rules 匹配规则列表
      * @param defaultMode 默认匹配方式
      * @param chars 分隔符
      * @return StringMatcher
-     * @see WrapStringMatcher#parseMatcher(String, String)
+     * @see Parsers#parseMatchers(String, LogicType, String, char...)
+     * @see Parsers#parseMatcher(String)
      * @since 5.2.0
      */
-    public static StringMatcher parseMatchers(String patterns, LogicType logicType, String defaultMode, char... chars) {
-        VerifyTools.requireNotBlank(patterns, "patterns");
-        if (chars == null || chars.length == 0) {
-            chars = new char[] { ',', '\n' };
+    public static StringMatcher parseMatchers(String rules, LogicType logicType, String defaultMode, char... chars) {
+        return MATCHER_PARSER.parseMatchers(rules, logicType, defaultMode, chars);
+    }
+
+    /**
+     * 解析StringMatcher规则列表
+     *
+     * @param rules 匹配规则列表
+     * @param defaultMode 默认匹配方式
+     * @return StringMatcher
+     * @see Parsers#parseMatchers(List, LogicType, String)
+     * @see Parsers#parseMatcher(String)
+     * @since 5.4.10
+     */
+    public static StringMatcher parseMatchers(List<String> rules, StringMatcher.LogicType logicType,
+            String defaultMode) {
+        return MATCHER_PARSER.parseMatchers(rules, logicType, defaultMode);
+    }
+
+    /**
+     * StringMatcher规则解析类
+     *
+     * @author zhaohuihua
+     * @version 20220206
+     */
+    public static class Parsers {
+
+        /** 按匹配模式解析的规则解析处理类 **/
+        private final Map<String, List<ParserByMode>> modeParsers = new HashMap<>();
+        /** 基础的规则解析处理类 **/
+        private final List<ParserByBase> baseParsers = new ArrayList<>();
+
+        public Parsers() {
+            this(true);
+        }
+
+        public Parsers(boolean addDefaultParsers) {
+            if (addDefaultParsers) {
+                addParser(new SimpleParser());
+            }
+        }
+
+        /** 增加规则解析处理类 **/
+        public void addParser(ParserByBase parser) {
+            this.baseParsers.add(parser);
+            if (parser instanceof RecursiveParser) {
+                ((RecursiveParser) parser).parsers = this;
+            }
+        }
+
+        /** 增加规则解析处理类 **/
+        public void addParser(ParserByMode parser) {
+            String[] supports = parser.supportModes();
+            if (supports == null || supports.length == 0) {
+                return;
+            }
+            if (parser instanceof RecursiveParser) {
+                ((RecursiveParser) parser).parsers = this;
+            }
+            for (String support : supports) {
+                if (this.modeParsers.containsKey(support)) {
+                    this.modeParsers.get(support).add(parser);
+                } else {
+                    List<ParserByMode> parsers = new ArrayList<>();
+                    parsers.add(parser);
+                    this.modeParsers.put(support, parsers);
+                }
+            }
+        }
+
+        /**
+         * 解析StringMatcher规则<br>
+         * rgp:或regexp:开头的解析为RegexpStringMatcher<br>
+         * ant:开头的解析为AntStringMatcher<br>
+         * eq:或equals:开头的解析为EqualsStringMatcher<br>
+         * ctn:或contains:开头的解析为ContainsStringMatcher<br>
+         * srt:或starts:开头的解析为StartsStringMatcher<br>
+         * end:或ends:开头的解析为EndsStringMatcher<br>
+         * 其余的解析为EqualsStringMatcher<br>
+         *
+         * @param rule 匹配规则
+         * @return StringMatcher
+         */
+        public StringMatcher parseMatcher(String rule) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseMatcher(rule, "equals");
+        }
+
+        /**
+         * 解析StringMatcher规则
+         *
+         * @param rule 匹配规则
+         * @param strict 是否使用严格格式
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatcher(String rule, boolean strict) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseMatcher(rule, strict ? "equals" : "contains");
+        }
+
+        /**
+         * 解析StringMatcher规则
+         *
+         * @param rule 匹配规则
+         * @param defaultMode 默认匹配方式
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatcher(String rule, String defaultMode) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseMatcherWithExcludeParser(rule, defaultMode, null);
+        }
+
+        protected StringMatcher parseMatcherWithExcludeParser(String rule, String defaultMode, Object excludeParser) {
+            StringMatcher matcher = doParseMatcherWithExcludeParser(rule, defaultMode, excludeParser);
+            return matcher != null ? matcher : new FixedStringMatcher(false);
+        }
+
+        private StringMatcher doParseMatcherWithExcludeParser(String rule, String defaultMode, Object excludeParser) {
+            // 先遍历判断解析器是否支持
+            for (ParserByBase parser : this.baseParsers) {
+                if (parser != null && parser != excludeParser && parser.supports(rule)) {
+                    return parser.parse(rule, defaultMode);
+                }
+            }
+
+            // 再按mode遍历解析器
+            RuleInfo ruleInfo = parseRuleInfo(rule, defaultMode);
+            String realRule = ruleInfo.rule;
+            String realMode = ruleInfo.mode;
+            Matches matchType = ruleInfo.matchType;
+            if (realMode != null && modeParsers.containsKey(realMode)) {
+                List<ParserByMode> parsers = modeParsers.get(realMode);
+                // 从后往前匹配, 同一类型下, 后加入的优先级最高
+                for (int i = parsers.size() - 1; i >= 0; i--) {
+                    ParserByMode parser = parsers.get(i);
+                    if (parser != null && parser != excludeParser) {
+                        StringMatcher matcher = parser.parse(realRule, realMode, defaultMode, matchType);
+                        if (matcher != null) {
+                            return matcher;
+                        }
+                    }
+                }
+            }
+
+            // 未找到解析器, 按EqualsStringMatcher处理
+            return new EqualsStringMatcher(rule);
+        }
+
+        private RuleInfo parseRuleInfo(String rule, String defaultMode) {
+            StringMatcher.Matches matchType = StringMatcher.Matches.Positive;
+            StringAccess sa = new StringAccess(rule);
+            String realMode = sa.skipWhitespace().readAscii();
+            if (realMode == null) {
+                return new RuleInfo(rule, defaultMode, matchType);
+            }
+            sa.skipWhitespace();
+            if (!sa.isReadable()) {
+                // 只有一个单词就到结尾了
+                if (modeParsers.containsKey(realMode)) {
+                    // 按无参数规则解析
+                    return new RuleInfo(null, realMode, matchType);
+                } else {
+                    // realMode作为参数解析
+                    return new RuleInfo(rule, defaultMode, matchType);
+                }
+            }
+            char next = sa.readChar();
+            if (next == '!') {
+                matchType = StringMatcher.Matches.Negative;
+                next = sa.skipWhitespace().readChar();
+            }
+            if (next != ':') {
+                return new RuleInfo(rule, defaultMode, matchType);
+            }
+            sa.skipWhitespace();
+            String realRule = sa.readToEnd();
+            return new RuleInfo(realRule, realMode, matchType);
+        }
+
+        /**
+         * 解析StringMatcher规则列表, 以逗号或换行符分隔
+         *
+         * @param rules 匹配规则列表
+         * @param logicType 多个匹配规则使用and还是or关联
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatchers(String rules, StringMatcher.LogicType logicType) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            return parseMatchers(rules, logicType, "equals", ',', '\n');
+        }
+
+        /**
+         * 解析StringMatcher规则列表<br>
+         * 如: parseMatchers(rule, Logic.OR, "ant", '\n'); // 默认以ant规则匹配
+         *
+         * @param rules 匹配规则列表
+         * @param defaultMode 默认匹配方式
+         * @param chars 分隔符
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatchers(String rules, StringMatcher.LogicType logicType, String defaultMode,
+                char... chars) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            if (chars == null || chars.length == 0) {
+                chars = new char[] {'\n'};
+            }
+            List<String> list = StringTools.splits(rules, chars);
+            return parseMatchers(list, logicType, defaultMode);
+        }
+
+        /**
+         * 解析StringMatcher规则列表<br>
+         * 如: parseMatchers(rule, Logic.OR, "ant", '\n'); // 默认以ant规则匹配
+         *
+         * @param rules 匹配规则列表
+         * @param defaultMode 默认匹配方式
+         * @return StringMatcher
+         * @see #parseMatcher(String)
+         */
+        public StringMatcher parseMatchers(List<String> rules, StringMatcher.LogicType logicType,
+                String defaultMode) {
+            List<StringMatcher> matchers = new ArrayList<>();
+            List<String> cleared = RuleTools.clearCommentLines(rules);
+            for (String rule : cleared) {
+                StringMatcher matcher = parseMatcher(rule, defaultMode);
+                if (matcher != null) {
+                    matchers.add(matcher);
+                }
+            }
+            if (matchers.size() == 0) {
+                return null;
+            } else if (matchers.size() == 1) {
+                return matchers.get(0);
+            } else {
+                return new WrapStringMatcher(logicType, matchers);
+            }
+        }
+    }
+
+    public static class FixedStringMatcher implements StringMatcher {
+
+        private final boolean result;
+
+        public FixedStringMatcher(boolean result) {
+            this.result = result;
+        }
+
+        @Override
+        public boolean matches(String source) {
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return "Always" + (result ? "True" : "False");
         }
-        String[] array = StringTools.split(patterns, chars);
-        List<StringMatcher> matchers = new ArrayList<>();
-        for (String pattern : array) {
-            if (VerifyTools.isNotBlank(pattern)) {
-                matchers.add(parseMatcher(pattern, defaultMode));
+    }
+
+    public static abstract class RecursiveParser {
+        private Parsers parsers;
+
+        public StringMatcher recursiveParse(String rule, String defaultMode) {
+            return parsers.parseMatcherWithExcludeParser(rule, defaultMode, this);
+        }
+
+        public List<StringMatcher> recursiveParse(List<String> rules, String defaultMode) {
+            List<StringMatcher> matchers = new ArrayList<>();
+            for (String item : rules) {
+                StringMatcher matcher = recursiveParse(item, defaultMode);
+                if (matcher != null) {
+                    matchers.add(matcher);
+                }
             }
+            return matchers;
         }
-        if (matchers.size() == 0) {
-            return null;
-        } else if (matchers.size() == 1) {
-            return matchers.get(0);
-        } else {
-            return new WrapStringMatcher(logicType, ConvertTools.toArray(matchers, StringMatcher.class));
+    }
+
+    /**
+     * 字符串匹配规则基础解析器
+     *
+     * @author zhaohuihua
+     * @version 20220120
+     */
+    private static class SimpleParser implements ParserByMode {
+
+        @Override
+        public String[] supportModes() {
+            List<String> types = new ArrayList<>();
+            types.add("regexp");
+            types.add("ant");
+            types.add("equals");
+            types.add("contains");
+            types.add("starts");
+            types.add("ends");
+
+            types.add("rgp");
+            types.add("eq");
+            types.add("ctn");
+            types.add("srt");
+            types.add("end");
+            return types.toArray(new String[] {});
+        }
+
+        @Override
+        public StringMatcher parse(String rule, String mode, String defaultMode, Matches matchType) {
+            if ("ant".equals(mode)) {
+                return new AntStringMatcher(rule, true, matchType);
+            } else if ("rgp".equals(mode) || "regexp".equals(mode)) {
+                return new RegexpStringMatcher(rule, matchType);
+            } else if ("eq".equals(mode) || "equals".equals(mode)) {
+                return new EqualsStringMatcher(rule, matchType);
+            } else if ("ctn".equals(mode) || "contains".equals(mode)) {
+                return new ContainsStringMatcher(rule, matchType);
+            } else if ("srt".equals(mode) || "starts".equals(mode)) {
+                return new StartsStringMatcher(rule, matchType);
+            } else if ("end".equals(mode) || "ends".equals(mode)) {
+                return new EndsStringMatcher(rule, matchType);
+            } else {
+                return null;
+            }
         }
     }
 
+    private static class RuleInfo {
+        private final String rule;
+        private final String mode;
+        private final Matches matchType;
+
+        public RuleInfo(String rule, String mode, Matches matchType) {
+            this.rule = rule;
+            this.mode = mode;
+            this.matchType = matchType;
+        }
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java
new file mode 100644
index 0000000000000000000000000000000000000000..e38898588ffacc8a7085a6ee4a749d16c06e1223
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/matches/WrapStringReplacer.java
@@ -0,0 +1,452 @@
+package com.gitee.qdbp.able.matches;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.parse.StringAccess;
+import com.gitee.qdbp.tools.utils.IndentTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * StringReplacer多规则的包装类
+ *
+ * @author zhaohuihua
+ * @version 20220206
+ * @since 5.4.10
+ */
+public class WrapStringReplacer implements StringReplacer {
+
+    private final List<StringReplacer> replacers = new ArrayList<>();
+
+    public WrapStringReplacer(List<StringReplacer> replacers) {
+        this.addReplacers(replacers);
+    }
+
+    public WrapStringReplacer(StringReplacer... replacers) {
+        this.addReplacers(replacers);
+    }
+
+    @Override
+    public String replace(String string) {
+        String target = string;
+        for (StringReplacer replacer : replacers) {
+            if (replacer != null) {
+                target = replacer.replace(target);
+            }
+        }
+        return target;
+    }
+
+    @Override
+    public String toString() {
+        if (this.replacers.isEmpty()) {
+            return "{null}";
+        }
+        StringBuilder buffer = new StringBuilder();
+        for (StringReplacer matcher : this.replacers) {
+            if (buffer.length() > 0) {
+                buffer.append('\n');
+            }
+            if (matcher instanceof WrapStringReplacer) {
+                String inner = IndentTools.space.tabAll(matcher.toString(), 1);
+                buffer.append("(\n").append(inner).append("\n)");
+            } else {
+                buffer.append(matcher.toString());
+            }
+        }
+        return buffer.toString();
+    }
+
+    /** 获取替换规则 **/
+    public List<StringReplacer> getReplacers() {
+        return replacers;
+    }
+
+    /** 设置替换规则 **/
+    public void setReplacers(List<StringReplacer> replacers) {
+        this.replacers.clear();
+        this.addReplacers(replacers);
+    }
+
+    /** 增加替换规则 **/
+    public void addReplacers(String... rules) {
+        if (rules != null && rules.length > 0) {
+            for (String rule : rules) {
+                this.replacers.add(parseReplacer(rule));
+            }
+        }
+    }
+
+    /** 增加替换规则 **/
+    public void addReplacers(StringReplacer... replacers) {
+        if (replacers != null && replacers.length > 0) {
+            Collections.addAll(this.replacers, replacers);
+        }
+    }
+
+    /** 增加替换规则 **/
+    public void addReplacers(List<StringReplacer> replacers) {
+        if (replacers != null && replacers.size() > 0) {
+            this.replacers.addAll(replacers);
+        }
+    }
+
+    private static final Parsers MATCHER_PARSER = new Parsers();
+
+    /**
+     * 解析文本替换规则
+     *
+     * @param rule 匹配规则
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacer(String rule) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return parseReplacer(rule, "regexp");
+    }
+
+    /**
+     * 解析文本替换规则
+     *
+     * @param rule 匹配规则
+     * @param defaultMode 默认替换方式
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacer(String, String)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacer(String rule, String defaultMode) {
+        VerifyTools.requireNotBlank(rule, "rule");
+        return MATCHER_PARSER.parseReplacer(rule, defaultMode);
+    }
+
+    /**
+     * 解析文本替换规则列表, 以逗号或换行符分隔
+     *
+     * @param rules 匹配规则列表
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacers(String)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacers(String rules) {
+        VerifyTools.requireNotBlank(rules, "rules");
+        return MATCHER_PARSER.parseReplacers(rules);
+    }
+
+    /**
+     * 解析文本替换规则列表
+     *
+     * @param rules 匹配规则列表
+     * @param defaultMode 默认替换方式
+     * @param chars 分隔符
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacers(String, String, char...)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacers(String rules, String defaultMode, char... chars) {
+        return MATCHER_PARSER.parseReplacers(rules, defaultMode, chars);
+    }
+
+    /**
+     * 解析文本替换规则列表
+     *
+     * @param rules 匹配规则列表
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacers(List)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacers(List<String> rules) {
+        return MATCHER_PARSER.parseReplacers(rules);
+    }
+
+    /**
+     * 解析文本替换规则列表
+     *
+     * @param rules 匹配规则列表
+     * @param defaultMode 默认替换方式
+     * @return StringReplacer
+     * @see WrapStringReplacer.Parsers#parseReplacers(List, String)
+     * @see WrapStringReplacer.Parsers#parseReplacer(String)
+     */
+    public static StringReplacer parseReplacers(List<String> rules, String defaultMode) {
+        return MATCHER_PARSER.parseReplacers(rules, defaultMode);
+    }
+
+    /**
+     * StringReplacer规则解析类
+     *
+     * @author zhaohuihua
+     * @version 20220206
+     */
+    public static class Parsers {
+
+        /** 按mode解析的规则解析处理类 **/
+        private final Map<String, List<ParserByMode>> modeParsers = new HashMap<>();
+        /** 基础的规则解析处理类 **/
+        private final List<ParserByBase> baseParsers = new ArrayList<>();
+
+        public Parsers() {
+            this(true);
+        }
+
+        public Parsers(boolean addDefaultParsers) {
+            if (addDefaultParsers) {
+                addParser(new DefaultParser());
+            }
+        }
+
+        /** 增加规则解析处理类 **/
+        public void addParser(ParserByBase parser) {
+            this.baseParsers.add(parser);
+            if (parser instanceof RecursiveParser) {
+                ((RecursiveParser) parser).parsers = this;
+            }
+        }
+
+        /** 增加规则解析处理类 **/
+        public void addParser(ParserByMode parser) {
+            String[] supports = parser.supportModes();
+            if (supports == null || supports.length == 0) {
+                return;
+            }
+            if (parser instanceof RecursiveParser) {
+                ((RecursiveParser) parser).parsers = this;
+            }
+            for (String support : supports) {
+                if (this.modeParsers.containsKey(support)) {
+                    this.modeParsers.get(support).add(parser);
+                } else {
+                    List<ParserByMode> parsers = new ArrayList<>();
+                    parsers.add(parser);
+                    this.modeParsers.put(support, parsers);
+                }
+            }
+        }
+
+        /**
+         * 解析文本替换规则<br>
+         * // 年初现金替换为期初现金<br>
+         * 年([初末])现金 -- 期$1现金<br>
+         * // 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字<br>
+         * (?<=所)支付的现金 -- 所支付的现金<br>
+         *
+         * @param rule 规则内容
+         * @return 规则对象
+         */
+        public StringReplacer parseReplacer(String rule) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseReplacer(rule, "regexp");
+        }
+
+        /**
+         * 解析文本替换规则
+         *
+         * @param rule 替换规则
+         * @param defaultMode 默认替换方式
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacer(String rule, String defaultMode) {
+            VerifyTools.requireNotBlank(rule, "rule");
+            return parseReplacerWithExcludeParser(rule, defaultMode, null);
+        }
+
+        protected StringReplacer parseReplacerWithExcludeParser(String rule, String defaultMode, Object excludeParser) {
+            StringReplacer Replacer = doParseReplacerWithExcludeParser(rule, defaultMode, excludeParser);
+            return Replacer != null ? Replacer : new NothingStringReplacer();
+        }
+
+        private StringReplacer doParseReplacerWithExcludeParser(String rule, String defaultMode, Object excludeParser) {
+            // 先遍历判断解析器是否支持
+            for (ParserByBase parser : this.baseParsers) {
+                if (parser != null && parser != excludeParser && parser.supports(rule)) {
+                    return parser.parse(rule, defaultMode);
+                }
+            }
+
+            // 再按mode遍历解析器
+            RuleInfo ruleInfo = parseRuleInfo(rule, defaultMode);
+            String realRule = ruleInfo.rule;
+            String realMode = ruleInfo.mode;
+            if (realMode != null && modeParsers.containsKey(realMode)) {
+                List<ParserByMode> parsers = modeParsers.get(realMode);
+                // 从后往前匹配, 同一类型下, 后加入的优先级最高
+                for (int i = parsers.size() - 1; i >= 0; i--) {
+                    ParserByMode parser = parsers.get(i);
+                    if (parser != null && parser != excludeParser) {
+                        StringReplacer replacer = parser.parse(realRule, realMode, defaultMode);
+                        if (replacer != null) {
+                            return replacer;
+                        }
+                    }
+                }
+            }
+
+            // 未找到解析器, 按RegexpStringReplacer处理
+            return RegexpStringReplacer.parseReplacer(rule);
+        }
+
+        private RuleInfo parseRuleInfo(String rule, String defaultMode) {
+            StringAccess sa = new StringAccess(rule);
+            String realMode = sa.skipWhitespace().readAscii();
+            if (realMode == null) {
+                return new RuleInfo(rule, defaultMode);
+            }
+            sa.skipWhitespace();
+            if (!sa.isReadable()) {
+                // 只有一个单词就到结尾了
+                if (modeParsers.containsKey(realMode)) {
+                    // 按无参数规则解析
+                    return new RuleInfo(null, realMode);
+                } else {
+                    // realMode作为参数解析
+                    return new RuleInfo(rule, defaultMode);
+                }
+            }
+            char next = sa.readChar();
+            if (next != ':') {
+                return new RuleInfo(rule, defaultMode);
+            }
+            sa.skipWhitespace();
+            String realRule = sa.readToEnd();
+            return new RuleInfo(realRule, realMode);
+        }
+
+        /**
+         * 解析文本替换规则列表, 以逗号或换行符分隔
+         *
+         * @param rules 替换规则列表
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacers(String rules) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            return parseReplacers(rules, "regexp", '\n');
+        }
+
+        /**
+         * 解析文本替换规则列表
+         *
+         * @param rules 替换规则列表
+         * @param defaultMode 默认替换方式
+         * @param chars 分隔符
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacers(String rules, String defaultMode, char... chars) {
+            VerifyTools.requireNotBlank(rules, "rules");
+            if (chars == null || chars.length == 0) {
+                chars = new char[] {'\n'};
+            }
+            List<String> list = StringTools.splits(rules, chars);
+            return parseReplacers(list, defaultMode);
+        }
+
+        /**
+         * 解析文本替换规则列表
+         *
+         * @param rules 替换规则列表
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacers(List<String> rules) {
+            return parseReplacers(rules, "regexp");
+        }
+
+        /**
+         * 解析文本替换规则列表
+         *
+         * @param rules 替换规则列表
+         * @param defaultMode 默认替换方式
+         * @return StringReplacer
+         * @see #parseReplacer(String)
+         */
+        public StringReplacer parseReplacers(List<String> rules, String defaultMode) {
+            List<StringReplacer> replacers = new ArrayList<>();
+            for (String rule : rules) {
+                if (VerifyTools.isNotBlank(rule)) {
+                    StringReplacer replacer = parseReplacer(rule, defaultMode);
+                    if (replacer != null) {
+                        replacers.add(replacer);
+                    }
+                }
+            }
+            if (replacers.size() == 0) {
+                return null;
+            } else if (replacers.size() == 1) {
+                return replacers.get(0);
+            } else {
+                return new WrapStringReplacer(replacers);
+            }
+        }
+    }
+
+    public static class NothingStringReplacer implements StringReplacer {
+
+        @Override
+        public String replace(String string) {
+            return string;
+        }
+
+        @Override
+        public String toString() {
+            return "Nothing";
+        }
+    }
+
+    public static abstract class RecursiveParser {
+        private Parsers parsers;
+
+        public StringReplacer recursiveParse(String rule, String defaultMode) {
+            return parsers.parseReplacerWithExcludeParser(rule, defaultMode, this);
+        }
+
+        public List<StringReplacer> recursiveParse(List<String> rules, String defaultMode) {
+            List<StringReplacer> Replacers = new ArrayList<>();
+            for (String item : rules) {
+                StringReplacer Replacer = recursiveParse(item, defaultMode);
+                if (Replacer != null) {
+                    Replacers.add(Replacer);
+                }
+            }
+            return Replacers;
+        }
+    }
+
+    /**
+     * 字符串替换规则基础解析器
+     *
+     * @author zhaohuihua
+     * @version 20220206
+     */
+    private static class DefaultParser implements ParserByMode {
+
+        @Override
+        public String[] supportModes() {
+            return new String[] { "rgp", "regexp", "simple" };
+        }
+
+        @Override
+        public StringReplacer parse(String rule, String mode, String defaultMode) {
+            if ("rgp".equals(mode) || "regexp".equals(mode)) {
+                return RegexpStringReplacer.parseReplacer(rule);
+            } else if ("simple".equals(mode)) {
+                return SimpleStringReplacer.parseReplacer(rule);
+            } else {
+                return null;
+            }
+        }
+    }
+
+    private static class RuleInfo {
+        private final String rule;
+        private final String mode;
+
+        public RuleInfo(String rule, String mode) {
+            this.rule = rule;
+            this.mode = mode;
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java b/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ed4af60b3014549093c5c15f8442ab1ec237440
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/model/file/PathInfo.java
@@ -0,0 +1,41 @@
+package com.gitee.qdbp.able.model.file;
+
+/**
+ * 路径信息
+ *
+ * @author zhaohuihua
+ * @version 20230312
+ * @since 5.5.10
+ */
+public interface PathInfo {
+
+    /** 文件所在目录 **/
+    String getDirectory();
+
+    /** 文件在目录下的相对路径 **/
+    String getRelativePath();
+
+    /** 完整路径 **/
+    String getPath();
+
+    /** 上级路径 **/
+    String getParent();
+
+    /** 文件名 **/
+    String getName();
+
+    /** 是不是文件 **/
+    boolean isFile();
+
+    /** 是不是目录 **/
+    boolean isDirectory();
+
+    /** 最后修改时间 **/
+    long lastModified();
+
+    /** 是否存在 **/
+    boolean exists();
+
+    /** 文件大小 **/
+    long length();
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionContext.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionContext.java
index 02edc1d9da6b8352ace0b9524262cbf174db53e3..86a459af5690ac6c885111f5fb7f7d98ffaa6317 100644
--- a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionContext.java
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionContext.java
@@ -23,7 +23,7 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
 public class ExpressionContext {
 
     /** 预置环境变量, 通过#{xxx}取值 **/
-    private final Map<String, Object> preset = new HashMap<>();
+    private final ExpressionMap preset = new ExpressionMap();
     /** 类的导入信息, key=&#64;类名简称, value=类名全称 **/
     private final Map<String, String> imports = new HashMap<>();
     /** 值栈包装类的构造函数 **/
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..7009e2a637c531bf8986400a1dcaf7b95d6c420b
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionExecutor.java
@@ -0,0 +1,168 @@
+package com.gitee.qdbp.able.model.reusable;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Map;
+import com.gitee.qdbp.able.exception.FormatException;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+
+/**
+ * ExpressionExecutor
+ *
+ * @author zhaohuihua
+ * @version 20210808
+ */
+public abstract class ExpressionExecutor extends ExpressionContext {
+
+    private static final Integer RESULT_SCALE = null;
+    private static final Integer CALC_SCALE = 18;
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression) throws FormatException {
+        return doEvaluate(expression, RESULT_SCALE, CALC_SCALE, null);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression, Map<String, Object> root) throws FormatException {
+        return doEvaluate(expression, RESULT_SCALE, CALC_SCALE, root);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param scale 结果保留几位小数 (只在结果是数字时生效)
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression, int scale, Map<String, Object> root) throws FormatException {
+        return doEvaluate(expression, scale, CALC_SCALE, root);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param resultScale 结果保留几位小数 (只在结果是数字时生效)
+     * @param calcScale 计算过程中的数字保留几位小数
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression, int resultScale, int calcScale, Map<String, Object> root)
+            throws FormatException {
+        return doEvaluate(expression, resultScale, calcScale, root);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param useBigDecimal 是否转换为BigDecimal以解决浮点数计算精度的问题
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    public Object evaluate(String expression, boolean useBigDecimal, Map<String, Object> root) throws FormatException {
+        return doEvaluate(expression, null, useBigDecimal ? CALC_SCALE : null, root);
+    }
+
+    /**
+     * 计算Ognl表达式的值
+     *
+     * @param expression 表达式
+     * @param resultScale 结果保留几位小数 (只在结果是数字时生效)
+     * @param calcScale 计算过程中的数字保留几位小数
+     * @param root 变量根容器
+     * @return 计算结果
+     * @throws FormatException 表达式语法错误, 表达式取值错误
+     */
+    protected Object doEvaluate(String expression, Integer resultScale, Integer calcScale, Map<String, Object> root)
+            throws FormatException {
+        // 替换静态方法及自定义方法名
+        String newExpression = replaceCustomizedFunctions(expression);
+        ExpressionMap map = new ExpressionMap();
+        if (root != null && !root.isEmpty()) {
+            map.putAll(root);
+        }
+
+        // 将值栈自身以ThisStack为名保存至栈中, 以实现在业务侧自定义值栈自身的函数, 如@contains(user.name)
+        // BaseContext.parseStackFunction将@contains(user.name)替换为ThisStack.contains('user.name')
+        StackWrapper thisStack = createStackWrapperInstance(map);
+        map.put("ThisStack", thisStack);
+
+        return evaluate(newExpression, resultScale, calcScale, map);
+    }
+
+    protected abstract Object evaluate(String expression, Integer resultScale, Integer calcScale,
+            Map<String, Object> root);
+
+    /**
+     * 如果对象是数字, 则转换为BigDecimal
+     * @param object 目标对象
+     * @param scale 小数位数
+     * @return 最终结果
+     */
+    public static Object tryConvertToDecimal(Object object, Integer scale) {
+        if (object instanceof BigDecimal) {
+            BigDecimal number = (BigDecimal) object;
+            if (number.scale() >= scale) {
+                return number;
+            } else {
+                return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
+            }
+        } else if (object instanceof BigInteger) {
+            return new BigDecimal(object.toString()).setScale(scale, BigDecimal.ROUND_HALF_UP);
+        } else if (object instanceof Number) {
+            return new BigDecimal(String.valueOf(object)).setScale(scale, BigDecimal.ROUND_HALF_UP);
+        } else {
+            return object;
+        }
+    }
+
+    /**
+     * 如果对象是数字, 则设置数字的精度
+     * @param object 目标对象
+     * @param scale 小数位数
+     * @return 最终结果
+     */
+    public static Object trySetDecimalScale(Object object, Integer scale) {
+        if (object instanceof BigDecimal) {
+            BigDecimal number = (BigDecimal) object;
+            if (scale == null) {
+                return ConvertTools.clearTrailingZeros(number);
+            } else if (number.scale() > scale) {
+                return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
+            }
+        } else if (object instanceof Double) {
+            BigDecimal number = new BigDecimal(String.valueOf(object));
+            if (scale == null) {
+                return ConvertTools.clearTrailingZeros(number);
+            } else if (number.scale() > scale) {
+                return number.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
+            }
+        } else if (object instanceof Float) {
+            BigDecimal number = new BigDecimal(String.valueOf(object));
+            if (scale == null) {
+                return ConvertTools.clearTrailingZeros(number);
+            } else if (number.scale() > scale) {
+                return number.setScale(scale, BigDecimal.ROUND_HALF_UP).floatValue();
+            }
+        }
+        return object;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionMap.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b611d4da93aa2382acef04fe0d34a84aa463602
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExpressionMap.java
@@ -0,0 +1,106 @@
+package com.gitee.qdbp.able.model.reusable;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 表达式Map, 获取数字时会转换为BigDecimal, 解决浮点数计算的精度问题
+ *
+ * @author zhaohuihua
+ * @version 20220317
+ */
+public class ExpressionMap extends HashMap<String, Object> {
+    private static final long serialVersionUID = -6647493913834307155L;
+    private static final Integer DECIMAL_CALC_SCALE = 18;
+
+    private Integer decimalCalcScale = DECIMAL_CALC_SCALE;
+
+    /** 构造函数 **/
+    public ExpressionMap() {
+    }
+
+    /** 构造函数 **/
+    public ExpressionMap(Map<String, Object> map) {
+        this.putAll(map);
+    }
+
+    /** 构造函数 **/
+    public ExpressionMap(String key, Object value) {
+        this.put(key, value);
+    }
+
+    /** 获取值, 如果值是数字, 将会转换为BigDecimal **/
+    @Override
+    public Object get(Object key) {
+        Object value = super.get(key);
+        return ExpressionExecutor.tryConvertToDecimal(value, decimalCalcScale);
+    }
+
+    /** 获取原值 **/
+    public Object getOriginal(Object key) {
+        return super.get(key);
+    }
+
+    @Override
+    public Object put(String key, Object value) {
+        return super.put(key, convertFieldToExpressionMap(value, decimalCalcScale));
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ?> map) {
+        for (Map.Entry<? extends String, ?> entry : map.entrySet()) {
+            this.put(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /** 数字在计算过程中保留的小数位数 **/
+    public Integer getDecimalCalcScale() {
+        return decimalCalcScale;
+    }
+
+    /** 数字在计算过程中保留的小数位数 **/
+    public void setDecimalCalcScale(Integer decimalCalcScale) {
+        this.decimalCalcScale = decimalCalcScale;
+    }
+
+    private static Object convertFieldToExpressionMap(Object object, int scale) {
+        if (object == null) {
+            return null;
+        }
+        if (object instanceof ExpressionMap) {
+            return object;
+        }
+        if (object instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) object;
+            ExpressionMap result = new ExpressionMap();
+            result.setDecimalCalcScale(scale);
+            for (Map.Entry<?, ?> entry : map.entrySet()) {
+                String key = entry.getKey() == null ? null : entry.getKey().toString();
+                result.put(key, convertFieldToExpressionMap(entry.getValue(), scale));
+            }
+            return result;
+        } else if (object.getClass().isArray()) {
+            List<Object> result = new ArrayList<>();
+            int size = Array.getLength(object);
+            for (int i = 0; i < size; i++) {
+                Object item = Array.get(object, i);
+                result.add(convertFieldToExpressionMap(item, scale));
+            }
+            return result.toArray();
+        } else if (object instanceof Iterable) {
+            Iterable<?> iterable = (Iterable<?>) object;
+            List<Object> result = new ArrayList<>();
+            for (Object item : iterable) {
+                result.add(convertFieldToExpressionMap(item, scale));
+            }
+            return result;
+        } else {
+            // 普通对象不能自动转换为map, 因为EL表达式中有可能会调用对象的方法
+            // Map<String, Object> map = JsonTools.beanToMap(object, true, false);
+            return object;
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraBase.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..2fdf49c19f7ac015ae2189a9c8c3f450c15acc6e
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraBase.java
@@ -0,0 +1,33 @@
+package com.gitee.qdbp.able.model.reusable;
+
+import java.util.Map;
+
+/**
+ * 附加数据
+ *
+ * @author zhaohuihua
+ * @version 20220130
+ */
+public interface ExtraBase {
+
+    /** 获取附加数据 **/
+    Map<String, Object> getExtra();
+
+    /** 设置附加数据 **/
+    void setExtra(Map<String, Object> extra);
+
+    /** 是否存在指定的附加数据 **/
+    boolean containsExtra(String key);
+
+    /** 获取指定的附加数据 **/
+    Object getExtra(String key);
+
+    /** 获取指定类型的附加数据 **/
+    <T> T getExtra(String key, Class<T> clazz);
+
+    /** 设置指定的附加数据 **/
+    void putExtra(String key, Object value);
+
+    /** 设置附加信息 **/
+    void putExtra(Map<String, ?> extra);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraData.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraData.java
index c671b03e433488d71ddfdaeb685f2ee164946c07..144c235783648bbed567eb4d249410e6d1f72a74 100644
--- a/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraData.java
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/ExtraData.java
@@ -11,7 +11,7 @@ import com.gitee.qdbp.tools.utils.MapTools;
  * @author zhaohuihua
  * @version 151020
  */
-public class ExtraData implements Serializable {
+public class ExtraData implements Serializable, ExtraBase {
 
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
@@ -19,32 +19,45 @@ public class ExtraData implements Serializable {
     /** 附加数据 **/
     private Map<String, Object> extra;
 
+    public ExtraData() {
+    }
+
+    public ExtraData(Map<String, Object> extra) {
+        this.extra = extra;
+    }
+
     /** 获取附加数据 **/
+    @Override
     public Map<String, Object> getExtra() {
         return extra;
     }
 
     /** 设置附加数据 **/
+    @Override
     public void setExtra(Map<String, Object> extra) {
         this.extra = extra;
     }
 
     /** 是否存在指定的附加数据 **/
+    @Override
     public boolean containsExtra(String key) {
         return this.extra != null && this.extra.containsKey(key);
     }
 
     /** 获取指定的附加数据 **/
+    @Override
     public Object getExtra(String key) {
         return this.extra == null ? null : this.extra.get(key);
     }
 
     /** 获取指定类型的附加数据 **/
+    @Override
     public <T> T getExtra(String key, Class<T> clazz) {
         return this.extra == null ? null : MapTools.getValue(this.extra, key, clazz);
     }
 
     /** 设置指定的附加数据 **/
+    @Override
     public void putExtra(String key, Object value) {
         if (this.extra == null) {
             this.extra = new HashMap<>();
@@ -53,7 +66,11 @@ public class ExtraData implements Serializable {
     }
 
     /** 设置附加信息 **/
+    @Override
     public void putExtra(Map<String, ?> extra) {
+        if (extra == null || extra.isEmpty()) {
+            return;
+        }
         if (this.extra == null) {
             this.extra = new HashMap<>();
         }
diff --git a/able/src/main/java/com/gitee/qdbp/able/model/reusable/StackWrapper.java b/able/src/main/java/com/gitee/qdbp/able/model/reusable/StackWrapper.java
index b2339bbe58d3c4136bb36243766db424b7b5ed5a..5d1e12dfb996b666c297d9e4a901b569c43dcf9e 100644
--- a/able/src/main/java/com/gitee/qdbp/able/model/reusable/StackWrapper.java
+++ b/able/src/main/java/com/gitee/qdbp/able/model/reusable/StackWrapper.java
@@ -21,7 +21,7 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  */
 public class StackWrapper {
 
-    private final Map<String, Object> map;
+    protected final Map<String, Object> map;
 
     public StackWrapper(Map<String, Object> map) {
         this.map = map;
diff --git a/able/src/main/java/com/gitee/qdbp/able/result/BatchResult.java b/able/src/main/java/com/gitee/qdbp/able/result/BatchResult.java
index 7e16a5dd58bfdf1e1aea9fada7cd6fa939cd853e..27c8c1a0a2ede846f8dc477ab1297b46c2cd80db 100644
--- a/able/src/main/java/com/gitee/qdbp/able/result/BatchResult.java
+++ b/able/src/main/java/com/gitee/qdbp/able/result/BatchResult.java
@@ -10,7 +10,7 @@ import java.util.List;
  * @author zhaohuihua
  * @version 170527
  */
-public class BatchResult implements IBatchResult, Serializable {
+public class BatchResult implements IBatchResult<BatchResult.FailedItem>, Serializable {
 
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
@@ -19,7 +19,7 @@ public class BatchResult implements IBatchResult, Serializable {
     private Integer total = 0;
 
     /** 失败列表 **/
-    private final List<Failed> failed = new ArrayList<>();
+    private final List<FailedItem> failed = new ArrayList<>();
 
     /** 整条记录失败 **/
     public void addFailed(int row, IResultMessage result) {
@@ -33,7 +33,7 @@ public class BatchResult implements IBatchResult, Serializable {
 
     /** 失败列表 **/
     @Override
-    public List<Failed> getFailed() {
+    public List<FailedItem> getFailed() {
         return failed;
     }
 
@@ -64,7 +64,7 @@ public class BatchResult implements IBatchResult, Serializable {
      * @author zhaohuihua
      * @version 160302
      */
-    public static class FailedItem implements Failed, Serializable {
+    public static class FailedItem implements IBatchResult.Failed, Serializable {
 
         /** 版本序列号 **/
         private static final long serialVersionUID = 1L;
diff --git a/able/src/main/java/com/gitee/qdbp/able/result/IBatchResult.java b/able/src/main/java/com/gitee/qdbp/able/result/IBatchResult.java
index bcd67d1be2e85dd3d128f2bd43329869b292e208..693909220a644b62cd6e2c10115db37dba6e876f 100644
--- a/able/src/main/java/com/gitee/qdbp/able/result/IBatchResult.java
+++ b/able/src/main/java/com/gitee/qdbp/able/result/IBatchResult.java
@@ -8,7 +8,7 @@ import java.util.List;
  * @author zhaohuihua
  * @version 150305
  */
-public interface IBatchResult {
+public interface IBatchResult<T extends IBatchResult.Failed> {
 
     /**
      * 失败记录
@@ -38,5 +38,5 @@ public interface IBatchResult {
      *
      * @return 失败列表
      */
-    List<Failed> getFailed();
+    List<T> getFailed();
 }
diff --git a/able/src/main/java/com/gitee/qdbp/able/result/IResultException.java b/able/src/main/java/com/gitee/qdbp/able/result/IResultException.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a6b17037f34db97e4362eb337a3c45cb03849f9
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/able/result/IResultException.java
@@ -0,0 +1,16 @@
+package com.gitee.qdbp.able.result;
+
+/**
+ * 操作异常
+ *
+ * @author zhaohuihua
+ * @version 20220710
+ */
+public interface IResultException extends IResultMessage {
+
+    /** 简单错误提示 **/
+    String getSimpleMessage();
+
+    /** 详细描述 **/
+    String getDetails();
+}
diff --git a/able/src/main/java/com/gitee/qdbp/able/time/BaseWorkdayResolver.java b/able/src/main/java/com/gitee/qdbp/able/time/BaseWorkdayResolver.java
index c898e5c27a2f12e9ce8b9ef26a6aa53931cb570d..594d2c390cee89d44816efeb6a18ea0dc7405f86 100644
--- a/able/src/main/java/com/gitee/qdbp/able/time/BaseWorkdayResolver.java
+++ b/able/src/main/java/com/gitee/qdbp/able/time/BaseWorkdayResolver.java
@@ -86,4 +86,30 @@ public abstract class BaseWorkdayResolver implements WorkdayResolver {
         }
         throw new IllegalStateException("Try " + max + " times and not found working day.");
     }
+
+    /** 统计两个日期之间存在多少个工作日 **/
+    public int countWorkday(Date startDate, Date endDate) {
+        Date start = DateTools.toStartTime(startDate);
+        Date end = DateTools.toStartTime(endDate);
+        if (start.getTime() == end.getTime()) {
+            return 0;
+        }
+        Calendar curr = Calendar.getInstance();
+        Calendar last = Calendar.getInstance();
+        if (start.before(end)) {
+            curr.setTime(start);
+            last.setTime(end);
+        } else {
+            curr.setTime(end);
+            last.setTime(start);
+        }
+        int total = 0;
+        while (curr.before(last)) {
+            curr.add(Calendar.DAY_OF_MONTH, 1);
+            if (isWorkday(curr)) {
+                total++;
+            }
+        }
+        return total;
+    }
 }
\ No newline at end of file
diff --git a/able/src/main/java/com/gitee/qdbp/able/time/FileWorkdayResolver.java b/able/src/main/java/com/gitee/qdbp/able/time/FileWorkdayResolver.java
index 7885d8dece5147ddb00b2547aa789787fc3a7f2e..15348715f8f6dd31ff9cdc50fae53e2a7ed049ac 100644
--- a/able/src/main/java/com/gitee/qdbp/able/time/FileWorkdayResolver.java
+++ b/able/src/main/java/com/gitee/qdbp/able/time/FileWorkdayResolver.java
@@ -30,7 +30,7 @@ public class FileWorkdayResolver extends SettingWorkdayResolver {
         try (InputStream input = url.openStream()) {
             readSettings(input);
         } catch (IOException e) {
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, PathTools.toUriPath(url), e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(PathTools.toUriPath(url));
         }
     }
 
@@ -41,7 +41,7 @@ public class FileWorkdayResolver extends SettingWorkdayResolver {
             String desc = "File location [" + file.getAbsolutePath() + "]";
             throw new ResourceNotFoundException(desc + " do not exist");
         } catch (IOException e) {
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, file.getAbsolutePath(), e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(file.getAbsolutePath());
         }
     }
 
@@ -52,7 +52,7 @@ public class FileWorkdayResolver extends SettingWorkdayResolver {
             String desc = "URL location [" + PathTools.toUriPath(url) + "]";
             throw new ResourceNotFoundException(desc + " do not exist");
         } catch (IOException e) {
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, url.toString(), e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(url.toString());
         }
     }
 
diff --git a/able/src/main/java/com/gitee/qdbp/able/time/SettingWorkdayResolver.java b/able/src/main/java/com/gitee/qdbp/able/time/SettingWorkdayResolver.java
index d95f5f124494807eb07fb8ddb9730a459de272b0..fa59779f488b260fbd4ac60e627fd2d75887e379 100644
--- a/able/src/main/java/com/gitee/qdbp/able/time/SettingWorkdayResolver.java
+++ b/able/src/main/java/com/gitee/qdbp/able/time/SettingWorkdayResolver.java
@@ -41,7 +41,7 @@ public abstract class SettingWorkdayResolver extends BaseWorkdayResolver {
         if (!isCached(day)) {
             findSettings(date.getTime());
         }
-        return workdays.containsKey(day) || !isWeekend(date) && !isHoliday(date);
+        return isWorkday(day, date);
     }
 
     @Override
@@ -57,10 +57,18 @@ public abstract class SettingWorkdayResolver extends BaseWorkdayResolver {
         if (!isCached(day)) {
             findSettings(date.getTime());
         }
+        return isHoliday(day, date);
+    }
+
+    protected boolean isWorkday(int day, Calendar date) {
+        return workdays.containsKey(day) || !isWeekend(date) && !isHoliday(day, date);
+    }
+
+    protected boolean isHoliday(int day, Calendar date) {
         return holidays.containsKey(day);
     }
 
-    private void findSettings(Date date) {
+    protected void findSettings(Date date) {
         // 整月的读取节假日, 并前后各多取1个月的数据
         Date minDate = DateTools.toFirstTime(DateTools.addMonth(date, -1), Calendar.MONTH);
         Date maxDate = DateTools.toLastTime(DateTools.addMonth(date, 1), Calendar.MONTH);
@@ -104,20 +112,20 @@ public abstract class SettingWorkdayResolver extends BaseWorkdayResolver {
         return months.containsKey(day / 100);
     }
 
-    private static int getDateValue(Date date) {
+    protected int getDateValue(Date date) {
         Calendar calendar = Calendar.getInstance();
         calendar.setTime(date);
         return getDateValue(calendar);
     }
 
-    private static int getDateValue(Calendar calendar) {
+    private int getDateValue(Calendar calendar) {
         int year = calendar.get(Calendar.YEAR);
         int month = calendar.get(Calendar.MONTH) + 1;
         int day = calendar.get(Calendar.DAY_OF_MONTH);
         return year * 10000 + month * 100 + day;
     }
 
-    protected static class Setting implements Serializable {
+    public static class Setting implements Serializable {
         private static final long serialVersionUID = -4206801241818872929L;
 
         private List<Date> holidays;
diff --git a/able/src/main/java/com/gitee/qdbp/able/time/WorkdayResolver.java b/able/src/main/java/com/gitee/qdbp/able/time/WorkdayResolver.java
index f0a793b03420f60db0f485de17e6ae5c2069dea3..b2c779bc2490798c40636c19517e7a1d1ba86713 100644
--- a/able/src/main/java/com/gitee/qdbp/able/time/WorkdayResolver.java
+++ b/able/src/main/java/com/gitee/qdbp/able/time/WorkdayResolver.java
@@ -28,4 +28,6 @@ public interface WorkdayResolver {
     /** 加几个工作日 (返回的日期必定是工作日) **/
     Date addWorkday(Date date, int offset);
 
+    /** 统计两个日期之间存在多少个工作日 **/
+    int countWorkday(Date start, Date end);
 }
diff --git a/able/src/main/java/com/gitee/qdbp/eachfile/process/BaseFileProcessor.java b/able/src/main/java/com/gitee/qdbp/eachfile/process/BaseFileProcessor.java
index cbb077bc65b2399d534f29536028eb21ff071a36..4fd39ad48d0fb6574a4fdacc37826a8cfee92bf8 100644
--- a/able/src/main/java/com/gitee/qdbp/eachfile/process/BaseFileProcessor.java
+++ b/able/src/main/java/com/gitee/qdbp/eachfile/process/BaseFileProcessor.java
@@ -46,11 +46,11 @@ public abstract class BaseFileProcessor implements FileProcessor, Debugger.Aware
     }
 
     @Override
-    public void init() {
+    public void init(String root) {
     }
 
     @Override
-    public void destroy() {
+    public void destroy(String root) {
     }
 
     protected void setIncludeFolders(String... includeFolders) {
diff --git a/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java b/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
index 4e57500d329dd2a2a5a05a87f17111315eb2efef..0c1842cf810b42bb812098334a6dabe1176a9adb 100644
--- a/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
+++ b/able/src/main/java/com/gitee/qdbp/eachfile/process/FileProcessor.java
@@ -11,10 +11,10 @@ import java.io.File;
 public interface FileProcessor {
 
     /** 初始化 **/
-    void init();
+    void init(String root);
 
     /** 销毁 **/
-    void destroy();
+    void destroy(String root);
 
     /**
      * 处理文件
@@ -29,7 +29,7 @@ public interface FileProcessor {
      * 
      * @param root 根目录
      * @param folder 待处理的文件夹
-     * @return 是否继续, false表示跳过后面的文件
+     * @return 是否继续, false表示跳过文件夹下面的文件
      */
     boolean folderProcess(String root, File folder);
 }
diff --git a/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java b/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
index 55a91f9cb3ef8842b00dc96e9bee7fd86325c522..9a5f4a456a59e936d2f63968a5126c0a9a8ec211 100644
--- a/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
+++ b/able/src/main/java/com/gitee/qdbp/eachfile/runner/FileEachRunner.java
@@ -2,14 +2,10 @@ package com.gitee.qdbp.eachfile.runner;
 
 import java.io.File;
 import java.io.FileFilter;
-import java.io.UnsupportedEncodingException;
-import java.net.URL;
-import java.net.URLDecoder;
 import com.gitee.qdbp.able.debug.ConsoleDebugger;
 import com.gitee.qdbp.able.debug.Debugger;
 import com.gitee.qdbp.eachfile.process.FileProcessor;
 import com.gitee.qdbp.tools.files.PathTools;
-import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.wait.SingleLock;
 
 /**
@@ -25,6 +21,7 @@ public class FileEachRunner extends Thread {
     private final FileFilter fileFilter;
     private final Debugger debugger;
     private SingleLock lock;
+    private boolean alive = true;
 
     public FileEachRunner(String root, FileProcessor processor) {
         this(root, null, processor, null);
@@ -46,20 +43,34 @@ public class FileEachRunner extends Thread {
 
     @Override
     public void run() {
+        try {
+            processor.init(root);
+        } catch (Exception e) {
+            debugger.debug("Failed to call init()", e);
+            return;
+        }
+
         File folder = new File(root);
         if (!checkRoot(folder)) {
             return;
         }
 
-        processor.init();
         try {
             each(folder);
+        } catch (Exception e) {
+            debugger.debug("Failed to each folder", e);
         } finally {
-            processor.destroy();
-        }
-
-        if (lock != null) {
-            lock.signal();
+            try {
+                processor.destroy(root);
+                debugger.debug("Success to each folder, the task has been finished!");
+            } catch (Exception e) {
+                debugger.debug("Failed to call destroy()", e);
+            } finally {
+                this.alive = false;
+                if (lock != null) {
+                    lock.signal();
+                }
+            }
         }
     }
 
@@ -70,7 +81,24 @@ public class FileEachRunner extends Thread {
         }
     }
 
+    /** 取消任务 **/
+    public void cancel(long delay) {
+        // 标记为结束
+        this.alive = false;
+        // 延时强杀
+        new StopTask(this, delay, debugger).start();
+    }
+
+    /** 任务是否仍在运行中 **/
+    public boolean isRunning() {
+        return this.alive;
+    }
+
     protected void each(File folder) {
+        if (!alive) {
+            debugger.debug("The task has been canceled!");
+            return;
+        }
         File[] files;
         if (fileFilter == null) {
             files = folder.listFiles();
@@ -81,6 +109,10 @@ public class FileEachRunner extends Thread {
             return;
         }
         for (File file : files) {
+            if (!alive) {
+                debugger.debug("The task has been canceled!");
+                return;
+            }
             if (file.isFile()) {
                 processor.fileProcess(root, file);
             } else if (file.isDirectory()) {
@@ -94,7 +126,7 @@ public class FileEachRunner extends Thread {
 
     protected boolean checkRoot(File root) {
         if (root.isFile()) {
-            debugger.debug("根路径必须是文件夹");
+            debugger.debug("The root path must be a folder");
             return false;
         } else {
             return true;
@@ -117,45 +149,28 @@ public class FileEachRunner extends Thread {
         return debugger;
     }
 
-    /**
-     * 获取指定类所在的classpath
-     * 
-     * @param clazz 指定类
-     * @return classpath
-     */
-    public static String getClasspath(Class<?> clazz) {
-        // 获取与JAR包自身所在的文件夹
-        // project-path/target/classes/
-        URL url = clazz.getResource("/");
-        if (url != null) {
-            return PathTools.toUriPath(url);
-        } else { // jar包中获取为空
-            // project-path/target/classes/com/gitee/qdbp/.../
-            // jar:file:/F:/project-path/xxx.jar!/com/gitee/qdbp/.../
-            url = clazz.getResource("");
-            assert url != null;
-            String p = PathTools.toUriPath(url);
-            p = StringTools.removePrefix(p, "jar://");
-            p = StringTools.removePrefix(p, "jar:");
-            p = StringTools.removePrefix(p, "file:");
-            int index = p.indexOf('!');
-            if (index > 0) {
-                p = PathTools.removeFileName(p.substring(0, index));
-            }
-            if ((p.length() > 2) && (p.charAt(2) == ':')) {
-                // "/c:/foo/" --> "c:/foo/"
-                p = p.substring(1);
+    private static class StopTask extends Thread {
+        private final Thread thread;
+        private final long delay;
+        private final Debugger debugger;
+
+        private StopTask(Thread thread, long delay, Debugger debugger) {
+            this.thread = thread;
+            this.delay = delay;
+            this.debugger = debugger;
+        }
+
+        @Override
+        @SuppressWarnings("deprecation")
+        public void run() {
+            try {
+                Thread.sleep(delay);
+            } catch (InterruptedException ignore) {
             }
-            if (!new File(p).exists() && p.contains("%")) {
-                try {
-                    p = URLDecoder.decode(p, "UTF-8");
-                    if (!new File(p).exists()) {
-                        p = URLDecoder.decode(p, "GBK");
-                    }
-                } catch (UnsupportedEncodingException ignore) {
-                }
+            if (thread.isAlive()) {
+                thread.stop();
+                debugger.debug("The task {} has been killed!", thread.getName());
             }
-            return p;
         }
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java b/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java
new file mode 100644
index 0000000000000000000000000000000000000000..96c76f93edf2c4a2317be092ef2801ac84f91686
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/json/JsonFeature.java
@@ -0,0 +1,391 @@
+package com.gitee.qdbp.json;
+
+import java.io.Serializable;
+import com.gitee.qdbp.able.beans.Copyable;
+import com.gitee.qdbp.able.beans.ModifyStatus;
+
+/**
+ * Json特征配置
+ *
+ * @author zhaohuihua
+ * @version 20211212
+ */
+public class JsonFeature {
+
+    private JsonFeature() {
+    }
+
+    public static Serialization serialization() {
+        return new Serialization();
+    }
+
+    public static Deserialization deserialization() {
+        return new Deserialization();
+    }
+
+    /**
+     * Json序列化的特征配置
+     *
+     * @author zhaohuihua
+     * @version 20211212
+     */
+    public static class Serialization implements Copyable, Serializable {
+        private static final long serialVersionUID = 176597457566019762L;
+        private final ModifyStatus modifyStatus = new ModifyStatus();
+        /** 是否处理循环引用 (设置为true就不会报错, 在fastjson下将会输出$ref, 在jackson下将会输出null) **/
+        private boolean handleCircularReference = true;
+        /** 字段名是否使用引号括起来 **/
+        private boolean quoteFieldNames = true;
+        /** 是否使用单引号 **/
+        private boolean useSingleQuotes = false;
+        /** 是否输出值为null的字段 **/
+        private boolean writeFieldNullValue = true;
+        /** 是否输出Map中的null值 **/
+        private boolean writeMapNullValue = true;
+        /** 输出枚举时是否使用toString **/
+        private boolean writeEnumUsingToString = false;
+        /** 序列化List类型数据时是否将null输出为空数组 **/
+        private boolean writeNullListAsEmpty = false;
+        /** 序列化String类型数据时是否将null输出为空字符串 **/
+        private boolean writeNullStringAsEmpty = false;
+        /** 序列化Number类型数据时是否将null输出为0 **/
+        private boolean writeNullNumberAsZero = false;
+        /** 序列化Boolean类型数据时是否将null输出为false **/
+        private boolean writeNullBooleanAsFalse = false;
+        /** 序列化BigDecimal类型数据时是否使用toPlainString (即不使用科学计数法显示) **/
+        private boolean writeBigDecimalAsPlain = true;
+        /** 序列化Date类型数据时是否输出为long时间戳 **/
+        private boolean writeDateAsTimestamp = false;
+        /** 是否跳过transient修饰的字段 **/
+        private boolean skipTransientField = true;
+        /** 是否输出格式化后的Json字符串 **/
+        private boolean prettyFormat = false;
+        /** 日期格式 (writeDateAsTimestamp=false时生效) **/
+        private String dateFormatPattern = "yyyy-MM-dd HH:mm:ss.SSS";
+        /** 数字格式 (防止double使用科学计数法, 默认情况下100000000.0将输出1.0E8) **/
+        private String doubleFormatPattern = "0.####################";
+
+        /** 是否处理循环引用 (设置为true就不会报错, 在fastjson下将会输出$ref, 在jackson下将会输出null) **/
+        public boolean isHandleCircularReference() {
+            return handleCircularReference;
+        }
+
+        /** 是否处理循环引用 (设置为true就不会报错, 在fastjson下将会输出$ref, 在jackson下将会输出null) **/
+        public Serialization setHandleCircularReference(boolean handleCircularReference) {
+            this.modifyStatus.checkModifiable();
+            this.handleCircularReference = handleCircularReference;
+            return this;
+        }
+
+        /** 字段名是否使用引号括起来 **/
+        public boolean isQuoteFieldNames() {
+            return quoteFieldNames;
+        }
+
+        /** 字段名是否使用引号括起来 **/
+        public Serialization setQuoteFieldNames(boolean quoteFieldNames) {
+            this.modifyStatus.checkModifiable();
+            this.quoteFieldNames = quoteFieldNames;
+            return this;
+        }
+
+        /** 是否使用单引号 **/
+        public boolean isUseSingleQuotes() {
+            return useSingleQuotes;
+        }
+
+        /** 是否使用单引号 **/
+        public Serialization setUseSingleQuotes(boolean useSingleQuotes) {
+            this.modifyStatus.checkModifiable();
+            this.useSingleQuotes = useSingleQuotes;
+            return this;
+        }
+
+        /** 是否输出值为null的字段 **/
+        public boolean isWriteFieldNullValue() {
+            return writeFieldNullValue;
+        }
+
+        /** 是否输出值为null的字段 **/
+        public Serialization setWriteFieldNullValue(boolean writeFieldNullValue) {
+            this.modifyStatus.checkModifiable();
+            this.writeFieldNullValue = writeFieldNullValue;
+            return this;
+        }
+
+        /** 是否输出Map中的null值 **/
+        public boolean isWriteMapNullValue() {
+            return writeMapNullValue;
+        }
+
+        /** 是否输出Map中的null值 **/
+        public Serialization setWriteMapNullValue(boolean writeMapNullValue) {
+            this.modifyStatus.checkModifiable();
+            this.writeMapNullValue = writeMapNullValue;
+            return this;
+        }
+
+        /** 输出枚举时是否使用toString **/
+        public boolean isWriteEnumUsingToString() {
+            return writeEnumUsingToString;
+        }
+
+        /** 输出枚举时是否使用toString **/
+        public Serialization setWriteEnumUsingToString(boolean writeEnumUsingToString) {
+            this.modifyStatus.checkModifiable();
+            this.writeEnumUsingToString = writeEnumUsingToString;
+            return this;
+        }
+
+        /** 序列化List类型数据时是否将null输出为空数组 **/
+        public boolean isWriteNullListAsEmpty() {
+            return writeNullListAsEmpty;
+        }
+
+        /** 序列化List类型数据时是否将null输出为空数组 **/
+        public Serialization setWriteNullListAsEmpty(boolean writeNullListAsEmpty) {
+            this.modifyStatus.checkModifiable();
+            this.writeNullListAsEmpty = writeNullListAsEmpty;
+            return this;
+        }
+
+        /** 序列化String类型数据时是否将null输出为空字符串 **/
+        public boolean isWriteNullStringAsEmpty() {
+            return writeNullStringAsEmpty;
+        }
+
+        /** 序列化String类型数据时是否将null输出为空字符串 **/
+        public Serialization setWriteNullStringAsEmpty(boolean writeNullStringAsEmpty) {
+            this.modifyStatus.checkModifiable();
+            this.writeNullStringAsEmpty = writeNullStringAsEmpty;
+            return this;
+        }
+
+        /** 序列化Number类型数据时是否将null输出为0 **/
+        public boolean isWriteNullNumberAsZero() {
+            return writeNullNumberAsZero;
+        }
+
+        /** 序列化Number类型数据时是否将null输出为0 **/
+        public Serialization setWriteNullNumberAsZero(boolean writeNullNumberAsZero) {
+            this.modifyStatus.checkModifiable();
+            this.writeNullNumberAsZero = writeNullNumberAsZero;
+            return this;
+        }
+
+        /** 序列化Boolean类型数据时是否将null输出为false **/
+        public boolean isWriteNullBooleanAsFalse() {
+            return writeNullBooleanAsFalse;
+        }
+
+        /** 序列化Boolean类型数据时是否将null输出为false **/
+        public Serialization setWriteNullBooleanAsFalse(boolean writeNullBooleanAsFalse) {
+            this.modifyStatus.checkModifiable();
+            this.writeNullBooleanAsFalse = writeNullBooleanAsFalse;
+            return this;
+        }
+
+        /** 序列化BigDecimal类型数据时是否使用toPlainString (即不使用科学计数法显示) **/
+        public boolean isWriteBigDecimalAsPlain() {
+            return writeBigDecimalAsPlain;
+        }
+
+        /** 序列化BigDecimal类型数据时是否使用toPlainString (即不使用科学计数法显示) **/
+        public Serialization setWriteBigDecimalAsPlain(boolean writeBigDecimalAsPlain) {
+            this.modifyStatus.checkModifiable();
+            this.writeBigDecimalAsPlain = writeBigDecimalAsPlain;
+            return this;
+        }
+
+        /** 序列化Date类型数据时是否输出为long时间戳 **/
+        public boolean isWriteDateAsTimestamp() {
+            return writeDateAsTimestamp;
+        }
+
+        /** 序列化Date类型数据时是否输出为long时间戳 **/
+        public Serialization setWriteDateAsTimestamp(boolean writeDateAsTimestamp) {
+            this.modifyStatus.checkModifiable();
+            this.writeDateAsTimestamp = writeDateAsTimestamp;
+            return this;
+        }
+
+        /** 是否跳过transient修饰的字段 **/
+        public boolean isSkipTransientField() {
+            return skipTransientField;
+        }
+
+        /** 是否跳过transient修饰的字段 **/
+        public Serialization setSkipTransientField(boolean skipTransientField) {
+            this.modifyStatus.checkModifiable();
+            this.skipTransientField = skipTransientField;
+            return this;
+        }
+
+        /** 是否输出格式化后的Json字符串 **/
+        public boolean isPrettyFormat() {
+            return prettyFormat;
+        }
+
+        /** 是否输出格式化后的Json字符串 **/
+        public Serialization setPrettyFormat(boolean prettyFormat) {
+            this.modifyStatus.checkModifiable();
+            this.prettyFormat = prettyFormat;
+            return this;
+        }
+
+        /** 日期格式 (writeDateAsTimestamp=false时生效) **/
+        public String getDateFormatPattern() {
+            return dateFormatPattern;
+        }
+
+        /** 日期格式 (writeDateAsTimestamp=false时生效) **/
+        public Serialization setDateFormatPattern(String dateFormatPattern) {
+            this.modifyStatus.checkModifiable();
+            this.dateFormatPattern = dateFormatPattern;
+            return this;
+        }
+
+        /** 数字格式 (防止double使用科学计数法) **/
+        public String getDoubleFormatPattern() {
+            return doubleFormatPattern;
+        }
+
+        /** 数字格式 (防止double使用科学计数法) **/
+        public Serialization setDoubleFormatPattern(String doubleFormatPattern) {
+            this.modifyStatus.checkModifiable();
+            this.doubleFormatPattern = doubleFormatPattern;
+            return this;
+        }
+
+        /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
+        public Serialization lockToUnmodifiable() {
+            this.modifyStatus.lockToUnmodifiable();
+            return this;
+        }
+
+        @Override
+        public Serialization copy() {
+            Serialization feature = new Serialization();
+            // 是否处理循环引用 (设置为true就不会报错, 在fastjson下将会输出$ref, 在jackson下将会输出null)
+            feature.setHandleCircularReference(this.isHandleCircularReference());
+            // 字段名是否使用引号括起来
+            feature.setQuoteFieldNames(this.isQuoteFieldNames());
+            // 是否使用单引号
+            feature.setUseSingleQuotes(this.isUseSingleQuotes());
+            // 是否输出值为null的字段
+            feature.setWriteFieldNullValue(this.isWriteFieldNullValue());
+            // 是否输出Map中的null值
+            feature.setWriteMapNullValue(this.isWriteMapNullValue());
+            // 输出枚举时是否使用toString
+            feature.setWriteEnumUsingToString(this.isWriteEnumUsingToString());
+            // 序列化List类型数据时是否将null输出为空数组
+            feature.setWriteNullListAsEmpty(this.isWriteNullListAsEmpty());
+            // 序列化String类型数据时是否将null输出为空字符串
+            feature.setWriteNullStringAsEmpty(this.isWriteNullStringAsEmpty());
+            // 序列化Number类型数据时是否将null输出为0
+            feature.setWriteNullNumberAsZero(this.isWriteNullNumberAsZero());
+            // 序列化Boolean类型数据时是否将null输出为false
+            feature.setWriteNullBooleanAsFalse(this.isWriteNullBooleanAsFalse());
+            // 序列化BigDecimal类型数据时是否使用toPlainString (即不使用科学计数法显示)
+            feature.setWriteBigDecimalAsPlain(this.isWriteBigDecimalAsPlain());
+            // 序列化Date类型数据时是否输出为long时间戳
+            feature.setWriteDateAsTimestamp(this.isWriteDateAsTimestamp());
+            // 是否跳过transient修饰的字段
+            feature.setSkipTransientField(this.isSkipTransientField());
+            // 是否输出格式化后的Json字符串
+            feature.setPrettyFormat(this.isPrettyFormat());
+            // 日期格式 (writeDateAsTimestamp=false时生效)
+            feature.setDateFormatPattern(this.getDateFormatPattern());
+            // 数字格式 (防止double使用科学计数法)
+            feature.setDoubleFormatPattern(this.getDoubleFormatPattern());
+            return feature;
+        }
+    }
+
+    /**
+     * Json反序列化的特征配置
+     *
+     * @author zhaohuihua
+     * @version 20211212
+     */
+    public static class Deserialization implements Copyable, Serializable {
+        private static final long serialVersionUID = -8068715156133134255L;
+        private final ModifyStatus modifyStatus = new ModifyStatus();
+        /** 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象 **/
+        private boolean useBigDecimalForFloats = true;
+        /** 遇到未知属性时是否抛出异常 **/
+        private boolean failOnUnknownFields = false;
+        /** 遇到未知的枚举值时是否抛出异常 **/
+        private boolean failOnUnknownEnumValues = false;
+        /** 是否将null字段串初始化为空字符串 **/
+        private boolean readNullStringAsEmpty = false;
+
+        /** 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象 **/
+        public boolean isUseBigDecimalForFloats() {
+            return useBigDecimalForFloats;
+        }
+
+        /** 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象 **/
+        public Deserialization setUseBigDecimalForFloats(boolean useBigDecimalForFloats) {
+            this.modifyStatus.checkModifiable();
+            this.useBigDecimalForFloats = useBigDecimalForFloats;
+            return this;
+        }
+
+        /** 遇到未知属性时是否抛出异常 **/
+        public boolean isFailOnUnknownFields() {
+            return failOnUnknownFields;
+        }
+
+        /** 遇到未知属性时是否抛出异常 **/
+        public Deserialization setFailOnUnknownFields(boolean failOnUnknownFields) {
+            this.modifyStatus.checkModifiable();
+            this.failOnUnknownFields = failOnUnknownFields;
+            return this;
+        }
+
+        /** 遇到未知属性时是否抛出异常 **/
+        public boolean isFailOnUnknownEnumValues() {
+            return failOnUnknownEnumValues;
+        }
+
+        /** 遇到未知属性时是否抛出异常 **/
+        public Deserialization setFailOnUnknownEnumValues(boolean failOnUnknownEnumValues) {
+            this.modifyStatus.checkModifiable();
+            this.failOnUnknownEnumValues = failOnUnknownEnumValues;
+            return this;
+        }
+
+        /** 是否将null字段串初始化为空字符串 **/
+        public boolean isReadNullStringAsEmpty() {
+            return readNullStringAsEmpty;
+        }
+
+        /** 是否将null字段串初始化为空字符串 **/
+        public Deserialization setReadNullStringAsEmpty(boolean readNullStringAsEmpty) {
+            this.modifyStatus.checkModifiable();
+            this.readNullStringAsEmpty = readNullStringAsEmpty;
+            return this;
+        }
+
+        /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
+        public Deserialization lockToUnmodifiable() {
+            modifyStatus.setModifiable(false);
+            return this;
+        }
+
+        @Override
+        public Deserialization copy() {
+            Deserialization feature = new Deserialization();
+            // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
+            feature.setUseBigDecimalForFloats(this.isUseBigDecimalForFloats());
+            // 遇到未知属性时是否抛出异常
+            feature.setFailOnUnknownFields(this.isFailOnUnknownFields());
+            // 遇到未知的枚举值时是否抛出异常
+            feature.setFailOnUnknownEnumValues(this.isFailOnUnknownEnumValues());
+            // 是否将null字段串初始化为空字符串
+            feature.setReadNullStringAsEmpty(this.isReadNullStringAsEmpty());
+            return feature;
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/json/JsonService.java b/able/src/main/java/com/gitee/qdbp/json/JsonService.java
new file mode 100644
index 0000000000000000000000000000000000000000..e014d63071354afb2cf88dc517712d2943261dd6
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/json/JsonService.java
@@ -0,0 +1,113 @@
+package com.gitee.qdbp.json;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Json转换服务
+ *
+ * @author zhaohuihua
+ * @version 20211207
+ */
+public interface JsonService {
+
+    /** 数据类型转换 **/
+    <T> T convert(Object object, Class<T> clazz);
+
+    /** 对象转换为Json字符串 **/
+    String toJsonString(Object object);
+
+    /** 对象转换为Json字符串 **/
+    String toJsonString(Object object, JsonFeature.Serialization feature);
+
+    /** 解析Json字符串对象 **/
+    Map<String, Object> parseAsMap(String jsonString);
+
+    /** 解析Json字符串对象 **/
+    Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature);
+
+    /** 解析Json字符串数组 **/
+    List<Map<String, Object>> parseAsMaps(String jsonString);
+
+    /** 解析Json字符串数组 **/
+    List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature);
+
+    /** 解析Json字符串对象 **/
+    <T> T parseAsObject(String jsonString, Class<T> clazz);
+
+    /** 解析Json字符串对象 **/
+    <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature);
+
+    /** 解析Json字符串数组 **/
+    <T> List<T> parseAsObjects(String jsonString, Class<T> clazz);
+
+    /** 解析Json字符串数组 **/
+    <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature);
+
+    /**
+     * 将Map内容设置到Java对象中<br>
+     * 遍历map中的key, 逐一设置到目标对象中
+     *
+     * @param map Map
+     * @param bean 目标Java对象
+     */
+    void mapFillToBean(Map<String, ?> map, Object bean);
+
+    /**
+     * 将Map转换为Java对象
+     *
+     * @param <T> 目标类型
+     * @param map Map
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    <T> T mapToBean(Map<String, ?> map, Class<T> clazz);
+
+    /**
+     * 将Java对象转换为Map
+     *
+     * @param bean JavaBean对象
+     * @return Map
+     */
+    Map<String, Object> beanToMap(Object bean);
+
+    /**
+     * 将Java对象转换为Map
+     *
+     * @param bean Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map
+     */
+    Map<String, Object> beanToMap(Object bean, boolean deep, boolean clearBlankValue);
+
+    /**
+     * 将Map数组转换为Java对象列表
+     *
+     * @param <T> 目标类型
+     * @param maps Map数组
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    <T, V> List<T> mapToBeans(Collection<Map<String, V>> maps, Class<T> clazz);
+
+    /**
+     * 将Java对象转换为Map列表<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param beans JavaBean对象列表
+     * @return Map列表
+     */
+    List<Map<String, Object>> beanToMaps(Collection<?> beans);
+
+    /**
+     * 将Java对象转换为Map列表
+     *
+     * @param beans Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map列表
+     */
+    List<Map<String, Object>> beanToMaps(Collection<?> beans, boolean deep, boolean clearBlankValue);
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java b/able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
new file mode 100644
index 0000000000000000000000000000000000000000..9fc63e420becb041f8af6c151df42cec979a5bb5
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/beans/MapPlus.java
@@ -0,0 +1,223 @@
+package com.gitee.qdbp.tools.beans;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.beans.ModifyStatus;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 增强的map
+ *
+ * @author zhaohuihua
+ * @version 20221207
+ */
+public class MapPlus extends LinkedHashMap<String, Object> {
+    private static final long serialVersionUID = 4479911245968765771L;
+
+    private final ModifyStatus modifyStatus = new ModifyStatus();
+
+    public MapPlus() {
+    }
+
+    public MapPlus(int initialCapacity) {
+        super(initialCapacity);
+    }
+
+    public MapPlus(int initialCapacity, float loadFactor) {
+        super(initialCapacity, loadFactor);
+    }
+
+    public MapPlus(Map<? extends String, ?> m) {
+        super();
+        putAll(m);
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ?> m) {
+        this.modifyStatus.checkModifiable();
+        if (m != null) {
+            for (Map.Entry<? extends String, ?> entry : m.entrySet()) {
+                put(entry.getKey(), entry.getValue());
+            }
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Object put(String key, Object value) {
+        this.modifyStatus.checkModifiable();
+        if (key == null) {
+            return super.put(null, value);
+        } else if (key.indexOf('.') <= 0) {
+            return super.put(key, value);
+        }
+
+        List<String> keys = StringTools.splits(key, '.');
+        Iterator<String> iterator = keys.iterator();
+
+        Map<String, Object> parent = this;
+        while (iterator.hasNext()) {
+            String name = iterator.next();
+
+            Object older = parent.get(name);
+            if (iterator.hasNext()) {
+                // 有下一级
+                if (older instanceof Map) {
+                    parent = (Map<String, Object>) older;
+                } else {
+                    // 如果之前有a.b.c, 新加一个a.b.c.d, 则用map覆盖掉a.b.c
+                    MapPlus newer = new MapPlus();
+                    parent.put(name, newer);
+                    parent = newer;
+                }
+            } else {
+                // 没有下一级
+                return parent.put(name, value);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Object remove(Object key) {
+        this.modifyStatus.checkModifiable();
+        if (key == null) {
+            return super.remove(null);
+        } else if (key instanceof String && ((String) key).indexOf('.') > 0) {
+            return MapTools.removeKey(this, (String) key);
+        } else {
+            return super.remove(key);
+        }
+    }
+
+    @Override
+    public Object get(Object key) {
+        if (key == null || super.containsKey(key)) {
+            return super.get(key);
+        }
+        String sk = key.toString();
+        if (isComplexKey(sk)) {
+            return ReflectTools.getDepthValue(this, sk);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 判断Map中是否存在指定的KEY
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 是否存在
+     */
+    @Override
+    public boolean containsKey(Object key) {
+        if (key == null || super.containsKey(key)) {
+            return super.containsKey(key);
+        }
+        String sk = key.toString();
+        if (isComplexKey(sk)) {
+            return ReflectTools.containsDepthFields(this, sk);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 获取字符串类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, MapTools.getString("xxx")将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 字符串
+     */
+    public String getString(String key) {
+        return MapTools.getString(this, key);
+    }
+
+    /**
+     * 获取指定类型的Map值<br>
+     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100(Integer), MapTools.getValue("xxx", Double.class)将返回null
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param clazz 指定类型
+     * @return Map值
+     */
+    public <T> T getValue(String key, Class<T> clazz) {
+        return MapTools.getValue(this, key, clazz);
+    }
+
+    /**
+     * 获取指定类型的Map值
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param defaults 值不存在或值类型不匹配时返回的默认值
+     * @param clazz 指定类型
+     * @return Map值
+     */
+    public <T> T getValue(String key, T defaults, Class<T> clazz) {
+        return MapTools.getValue(this, key, defaults, clazz);
+    }
+
+    /**
+     * 获取指定类型的Map值列表
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @param clazz 指定类型
+     * @return Map值列表, 值不存在时将返回空List而不是null
+     */
+    public <T> List<T> getList(String key, Class<T> clazz) {
+        return MapTools.getList(this, key, clazz);
+    }
+
+    /**
+     * 获取子Map
+     *
+     * @param key KEY, 支持多级, 如user.addresses[0]
+     * @return 子Map, 值不存在时将返回空Map而不是null
+     */
+    public Map<String, Object> getSubMap(String key) {
+        return MapTools.getSubMap(this, key);
+    }
+
+    /**
+     * 获取子Map数组
+     *
+     * @param key KEY, 支持多级, 如user.addresses
+     * @return 子Map数组, 值不存在时将返回空List而不是null
+     */
+    public List<Map<String, Object>> getSubMaps(String key) {
+        return MapTools.getSubMaps(this, key);
+    }
+
+    /** 作为全局配置对象使用时, 可调用该方法锁定为不可修改状态 **/
+    public MapPlus lockToUnmodifiable() {
+        this.modifyStatus.lockToUnmodifiable();
+        return this;
+    }
+
+    protected boolean isComplexKey(String key) {
+        return key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0;
+    }
+
+    /**
+     * 将任意Map转换为MapPlus
+     *
+     * @param map 源MapPlus
+     * @return MapPlus
+     */
+    public static MapPlus of(Map<?, ?> map) {
+        MapPlus result = new MapPlus();
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            Object key = entry.getKey();
+            if (key == null) {
+                result.put(null, entry.getValue());
+            } else {
+                result.put(key.toString(), entry.getValue());
+            }
+        }
+        return result;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/codec/CodeTools.java b/able/src/main/java/com/gitee/qdbp/tools/codec/CodeTools.java
index cd59c302ab32634aa5f2236bbf2407e2ce300046..a5f1608553c1e7861d8619e8ba8448f1457e2811 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/codec/CodeTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/codec/CodeTools.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.tools.codec;
 
 import java.util.ArrayList;
 import java.util.List;
+import com.gitee.qdbp.tools.utils.StringTools;
 
 /**
  * 编号处理工具类<br>
@@ -47,6 +48,11 @@ public class CodeTools {
         return prefix(code, start, length);
     }
 
+    /** 在上级的基础上追加序号生成子级编号 **/
+    public String pad(String parent, int index) {
+        return parent + StringTools.pad(index, length);
+    }
+
     /**
      * 拆分编号<br>
      * 如split("500100220033", 4, true) --&gt; [5001, 50010022, 500100220033]<br>
diff --git a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java
new file mode 100644
index 0000000000000000000000000000000000000000..cf768481b6fe4190d07245f99b30332761b3d4e3
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareStream.java
@@ -0,0 +1,198 @@
+package com.gitee.qdbp.tools.compare;
+
+import java.util.ArrayList;
+import java.util.List;
+import com.gitee.qdbp.able.function.BaseFunction;
+
+/**
+ * 流式比较容器
+ *
+ * @author zhaohuihua
+ * @version 20220113
+ */
+public class CompareStream {
+    private final List<CompareItem> items = new ArrayList<>();
+
+    protected CompareStream() {
+    }
+
+    /**
+     * 升序比较 (空值排最后)
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param <T> 对象类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T extends Comparable<T>> CompareStream asc(T o1, T o2) {
+        this.items.add(new CompareItemForObject<>(o1, o2, true, false));
+        return this;
+    }
+
+    /**
+     * 升序比较
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @param <T> 对象类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T extends Comparable<T>> CompareStream asc(T o1, T o2, boolean nullsLow) {
+        this.items.add(new CompareItemForObject<>(o1, o2, true, nullsLow));
+        return this;
+    }
+
+    /**
+     * 降序比较 (空值排最后)
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param <T> 对象类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T extends Comparable<T>> CompareStream desc(T o1, T o2) {
+        this.items.add(new CompareItemForObject<>(o1, o2, false, false));
+        return this;
+    }
+
+    /**
+     * 降序比较
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @param <T> 对象类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T extends Comparable<T>> CompareStream desc(T o1, T o2, boolean nullsLow) {
+        this.items.add(new CompareItemForObject<>(o1, o2, false, false));
+        return this;
+    }
+
+    /**
+     * 先进行计算, 再对计算结果升序比较 (空值排最后)
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param function 计算函数 (先对o1,o2进行计算再比较)
+     * @param <T> 对象类型
+     * @param <C> 中间计算结果类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T, C extends Comparable<C>> CompareStream asc(T o1, T o2, BaseFunction<T, C> function) {
+        this.items.add(new CompareItemForFunction<>(o1, o2, true, false, function));
+        return this;
+    }
+
+    /**
+     * 先进行计算, 再对计算结果升序比较
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @param function 计算函数 (先对o1,o2进行计算再比较)
+     * @param <T> 对象类型
+     * @param <C> 中间计算结果类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T, C extends Comparable<C>> CompareStream asc(T o1, T o2, boolean nullsLow,
+            BaseFunction<T, C> function) {
+        this.items.add(new CompareItemForFunction<>(o1, o2, true, nullsLow, function));
+        return this;
+    }
+
+    /**
+     * 先进行计算, 再对计算结果降序比较 (空值排最后)
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param function 计算函数 (先对o1,o2进行计算再比较)
+     * @param <T> 对象类型
+     * @param <C> 中间计算结果类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T, C extends Comparable<C>> CompareStream desc(T o1, T o2, BaseFunction<T, C> function) {
+        this.items.add(new CompareItemForFunction<>(o1, o2, false, false, function));
+        return this;
+    }
+
+    /**
+     * 先进行计算, 再对计算结果降序比较
+     *
+     * @param o1 对象1
+     * @param o2 对象2
+     * @param nullsLow 空值优先级: true=空值排最前, false=空值排最后
+     * @param function 计算函数 (先对o1,o2进行计算再比较)
+     * @param <T> 对象类型
+     * @param <C> 中间计算结果类型
+     * @return 返回对象自身用于链式调用
+     */
+    public <T, C extends Comparable<C>> CompareStream desc(T o1, T o2, boolean nullsLow,
+            BaseFunction<T, C> function) {
+        this.items.add(new CompareItemForFunction<>(o1, o2, false, false, function));
+        return this;
+    }
+
+    /**
+     * 逐一比较对象, 遇到首个不为0的结果就返回
+     *
+     * @return 小于返回1/等于返回0/大于返回-1
+     */
+    public int compare() {
+        for (CompareItem item : items) {
+            int result = item.compare();
+            if (result != 0) {
+                return result;
+            }
+        }
+        return 0;
+    }
+
+    private interface CompareItem {
+        int compare();
+    }
+
+    private static class CompareItemForObject<T extends Comparable<T>> implements CompareItem {
+        private final T o1;
+        private final T o2;
+        private final boolean ascending;
+        private final boolean nullsLow;
+
+        private CompareItemForObject(T o1, T o2, boolean ascending, boolean nullsLow) {
+            this.o1 = o1;
+            this.o2 = o2;
+            this.ascending = ascending;
+            this.nullsLow = nullsLow;
+        }
+
+        @Override
+        public int compare() {
+            return CompareTools.compare(o1, o2, ascending, nullsLow);
+        }
+    }
+
+    private static class CompareItemForFunction<T, C extends Comparable<C>> implements CompareItem {
+        private final T o1;
+        private final T o2;
+        private final boolean ascending;
+        private final boolean nullsLow;
+        private final BaseFunction<T, C> function;
+
+        private CompareItemForFunction(T o1, T o2, boolean ascending, boolean nullsLow,
+                BaseFunction<T, C> function) {
+            this.o1 = o1;
+            this.o2 = o2;
+            this.ascending = ascending;
+            this.nullsLow = nullsLow;
+            this.function = function;
+        }
+
+        @Override
+        public int compare() {
+            C c1 = function.apply(o1);
+            C c2 = function.apply(o2);
+            return CompareTools.compare(c1, c2, ascending, nullsLow);
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
index 3dad859ae39da764a0e0efa801d66728d0bfc657..db30486b4a995d3c047be1a0cf3911d427e0ad59 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/compare/CompareTools.java
@@ -50,6 +50,7 @@ public class CompareTools {
      * @param list 列表数据
      * @param orderings 排序规则, e.g. Orderings.of("dept desc, birthday asc, name desc");
      */
+    @SuppressWarnings("all")
     public static void sort(List<Map<String, Object>> list, Orderings orderings) {
         if (list == null || list.isEmpty() || orderings == null || orderings.isEmpty()) {
             return;
@@ -72,6 +73,11 @@ public class CompareTools {
         return new MapFieldComparator<>(orderBy, ascending);
     }
 
+    /** 链式比较多个字段 **/
+    public static CompareStream stream() {
+        return new CompareStream();
+    }
+
     /**
      * 升序比较 (空值排最后)
      *
@@ -122,7 +128,7 @@ public class CompareTools {
         return compare(o1, o2, false, nullsLow);
     }
 
-    private static <T extends Comparable<T>> int compare(T o1, T o2, boolean ascending, boolean nullsLow) {
+    protected static <T extends Comparable<T>> int compare(T o1, T o2, boolean ascending, boolean nullsLow) {
         if (o1 == o2) {
             return 0;
         }
@@ -144,7 +150,7 @@ public class CompareTools {
      * @param source 原文本
      * @param target 目标文本
      * @param findMode 是否为查找模式
-     * @param ignoreChars 忽略字符, 如标点符号以及"的了吗吧"
+     * @param ignoreChars 忽略字符, 如标点符号
      * @return 差异信息
      */
     public static DiffInfo textCompare(String source, String target, boolean findMode, String ignoreChars) {
@@ -232,8 +238,15 @@ public class CompareTools {
                 }
             }
             if (prevDelete != null) {
-                isMatched = false;
-                diffItems.add(new DiffItem(DiffType.delete, prevDelete.text));
+                // 原文比目标多的部分
+                boolean isLike = isIgnoreWords(prevDelete.text, ignoreChars);
+                if (isLike) {
+                    equalsTotal += prevDelete.text.length();
+                    diffItems.add(new DiffItem(DiffType.like, prevDelete.text));
+                } else {
+                    isMatched = false;
+                    diffItems.add(new DiffItem(DiffType.delete, prevDelete.text));
+                }
                 prevDelete = null;
             }
             if (item.operation == Operation.DELETE) {
@@ -241,6 +254,7 @@ public class CompareTools {
                 continue;
             }
             if (item.operation == Operation.INSERT) {
+                // 原文比目标少的部分
                 boolean isLike = isIgnoreWords(item.text, ignoreChars);
                 if (isLike) {
                     equalsTotal += item.text.length();
@@ -255,16 +269,31 @@ public class CompareTools {
             }
         }
         if (prevDelete != null) {
-            isMatched = false;
-            diffItems.add(new DiffItem(DiffType.delete, prevDelete.text));
+            boolean isLike = isIgnoreWords(prevDelete.text, ignoreChars);
+            if (isLike) {
+                equalsTotal += prevDelete.text.length();
+                diffItems.add(new DiffItem(DiffType.like, prevDelete.text));
+            } else {
+                isMatched = false;
+                diffItems.add(new DiffItem(DiffType.delete, prevDelete.text));
+            }
+        }
+
+        int sourceLength = source.length() - prefixLength - suffixLength;
+        int targetLength = target.length();
+
+        double score = 1.0D;
+        if (!isMatched) {
+            // 分数: 相等部分占目标长度的比率占75%, 相等部分占原文长度的比率占25%
+            score = 0.75 * equalsTotal / targetLength + 0.25 * equalsTotal / sourceLength;
         }
 
         DiffInfo result = new DiffInfo();
         result.setMatched(isMatched);
-        result.setScore(1.0 * equalsTotal / target.length());
+        result.setScore(score);
         result.setDetails(diffItems);
         result.setStartIndex(prefixLength);
-        result.setEndIndex(target.length() - suffixLength);
+        result.setEndIndex(source.length() - suffixLength);
         return result;
     }
 
@@ -322,7 +351,13 @@ public class CompareTools {
                     if (item.getText() != null) {
                         text = doEscapeXml(item.getText());
                     }
-                    String title = doEscapeXml(item.getReplacement());
+                    String title;
+                    if (item.getText() != null && item.getReplacement() == null) {
+                        // 有原文没有替换内容, 就是删除
+                        title = deleteTitle;
+                    } else {
+                        title = doEscapeXml(item.getReplacement());
+                    }
                     appendHtmlTag(buffer, span, type, text, likeTitlePrefix, title);
                     break;
                 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java
new file mode 100644
index 0000000000000000000000000000000000000000..9ac3dcaa45a28facf4b9829d0e1283a680eaf82e
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesCipher.java
@@ -0,0 +1,346 @@
+package com.gitee.qdbp.tools.crypto;
+
+import javax.crypto.SecretKey;
+import com.gitee.qdbp.tools.codec.bytes.ByteCodec;
+import com.gitee.qdbp.tools.codec.bytes.HexCodec;
+import com.gitee.qdbp.tools.codec.bytes.TextCodec;
+import com.gitee.qdbp.tools.utils.RandomTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+
+/**
+ * AES加解密实例
+ *
+ * @author zhaohuihua
+ * @version 20200510
+ */
+public class AesCipher {
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** 生成安全密钥 **/
+    public static byte[] generateSecretKey() {
+        return AesTools.generateSecretKey();
+    }
+
+    /** 生成随机CBC初始向量 **/
+    public static byte[] generateCbcInitVector() {
+        return AesTools.generateCbcInitVector();
+    }
+
+    public static class Options {
+        /** 输入输出文本的编解码方式 **/
+        private TextCodec textCodec = TextCodec.UTF8;
+        /** 输入输出Byte的编解码方式 **/
+        private ByteCodec byteCodec = HexCodec.INSTANCE;
+        /** 密钥对象 **/
+        private SecretKey secretKey;
+        /** 填充方式, 默认为 PKCS5Padding **/
+        private String padding;
+        /** 增加几位随机字符参与加密 **/
+        private int randomChars;
+
+        /** 输入输出文本的编解码方式 **/
+        public TextCodec getTextCodec() {
+            return textCodec;
+        }
+
+        /** 输入输出Byte的编解码方式 **/
+        public ByteCodec getByteCodec() {
+            return byteCodec;
+        }
+
+        /** 密钥Bytes **/
+        public byte[] getSecretBytes() {
+            return secretKey.getEncoded();
+        }
+
+        /** 密钥字符串 **/
+        public String getSecretKey() {
+            return byteCodec.encode(secretKey.getEncoded());
+        }
+
+        /** 密钥字符串 **/
+        public String getSecretKey(ByteCodec byteCodec) {
+            return byteCodec.encode(secretKey.getEncoded());
+        }
+
+        /** 填充方式, 默认为 PKCS5Padding **/
+        public String getPadding() {
+            return padding;
+        }
+
+        /** 增加几位随机字符参与加密 **/
+        public int getRandomChars() {
+            return randomChars;
+        }
+    }
+
+    public static class Builder {
+
+        private final Options o = new Options();
+
+        /** 输入输出文本的编解码方式 **/
+        public Builder textCodec(TextCodec textCodec) {
+            VerifyTools.requireNonNull(textCodec, "textCodec");
+            this.o.textCodec = textCodec;
+            return this;
+        }
+
+        /** 输入输出Byte的编解码方式 **/
+        public Builder byteCodec(ByteCodec byteCodec) {
+            VerifyTools.requireNonNull(byteCodec, "byteCodec");
+            this.o.byteCodec = byteCodec;
+            return this;
+        }
+
+        /** 生成密钥 **/
+        public Builder generateKey() {
+            byte[] secretKey = generateSecretKey();
+            this.o.secretKey = AesTools.generateSecretKey(secretKey, false);
+            return this;
+        }
+
+        /** 密钥字符串 **/
+        public Builder secretKey(String stringKey) {
+            VerifyTools.requireNotBlank(stringKey, "stringKey");
+            return secretKey(stringKey, TextCodec.UTF8, true);
+        }
+
+        /** 密钥字符串 **/
+        public Builder secretKey(String stringKey, ByteCodec keyCodec) {
+            VerifyTools.requireNotBlank(stringKey, "stringKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            // TextCodec相当于string.getBytes(), 即表明密钥是字符串, 长度一般不符合要求
+            // HexCodec/Base64Codec/Base58Codec, 一般说明密钥是byte[]
+            boolean keyPadding = keyCodec instanceof TextCodec;
+            return secretKey(stringKey, keyCodec, keyPadding);
+        }
+
+        /** 密钥字符串 **/
+        public Builder secretKey(String stringKey, ByteCodec keyCodec, boolean keyPadding) {
+            VerifyTools.requireNotBlank(stringKey, "stringKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            this.o.secretKey = AesTools.generateSecretKey(keyCodec.decode(stringKey), keyPadding);
+            return this;
+        }
+
+        /** 密钥Bytes, 长度必须为16/24/32 **/
+        public Builder secretKey(byte[] secretKey) {
+            VerifyTools.requireNonNull(secretKey, "secretKey");
+            return secretKey(secretKey, false);
+        }
+
+        /** 密钥Bytes, 长度不限 **/
+        public Builder secretKey(byte[] secretKey, boolean keyPadding) {
+            VerifyTools.requireNonNull(secretKey, "secretKey");
+            this.o.secretKey = AesTools.generateSecretKey(secretKey, keyPadding);
+            return this;
+        }
+
+        /** 密钥对象 **/
+        public Builder secretKey(SecretKey secretKey) {
+            VerifyTools.requireNonNull(secretKey, "secretKey");
+            this.o.secretKey = secretKey;
+            return this;
+        }
+
+        /** 填充方式, 默认为 PKCS5Padding **/
+        public Builder padding(String padding) {
+            VerifyTools.requireNotBlank(padding, "padding");
+            this.o.padding = padding;
+            return this;
+        }
+
+        /** 增加几位随机字符参与加密 **/
+        public Builder randomChars(int randomChars) {
+            this.o.randomChars = randomChars;
+            return this;
+        }
+
+        public EcbCipher buildEcbCipher() {
+            if (o.secretKey == null) {
+                throw new IllegalStateException("SecretKey not configured");
+            }
+            return new EcbCipher(o);
+        }
+
+        public CbcDefault buildCbcCipher() {
+            if (o.secretKey == null) {
+                throw new IllegalStateException("SecretKey not configured");
+            }
+            return new CbcDefault(o);
+        }
+    }
+
+    /** 默认CbcCipher, 固定使用0作为初始向量 **/
+    public static class CbcDefault extends BaseCipher {
+
+        private CbcDefault(Options options) {
+            super(options);
+        }
+
+        /** 生成随机初始向量 **/
+        public CbcCipher generateInitVector() {
+            byte[] initVector = generateCbcInitVector();
+            return new CbcCipher(o, initVector);
+        }
+
+        /** 使用指定的初始向量 (长度必须为16位) **/
+        public CbcCipher initVector(byte[] initVector) {
+            return new CbcCipher(o, initVector);
+        }
+
+        /** 使用字符串初始向量 (长度必须为16位) **/
+        public CbcCipher initVector(String stringKey) {
+            byte[] initVector = TextCodec.UTF8.decode(stringKey);
+            return new CbcCipher(o, initVector);
+        }
+
+        /** 使用字符串初始向量 (解析为byte[]后长度必须为16位) **/
+        public CbcCipher initVector(String stringKey, ByteCodec keyCodec) {
+            byte[] initVector = keyCodec.decode(stringKey);
+            return new CbcCipher(o, initVector);
+        }
+
+        @Override
+        protected byte[] doEncrypt(byte[] plaintext) {
+            byte[] initVector = new byte[16];
+            return AesTools.cbcEncrypt(plaintext, initVector, o.padding, o.secretKey);
+        }
+
+        @Override
+        protected byte[] doDecrypt(byte[] ciphertext) {
+            byte[] initVector = new byte[16];
+            return AesTools.cbcDecrypt(ciphertext, initVector, o.padding, o.secretKey);
+        }
+    }
+
+    /** AES/CBC加密解密类 **/
+    public static class CbcCipher extends BaseCipher {
+
+        private final byte[] initVector;
+
+        private CbcCipher(Options options, byte[] initVector) {
+            super(options);
+            this.initVector = initVector;
+        }
+
+        public byte[] getInitVectorBytes() {
+            return initVector;
+        }
+
+        public String getInitVectorString() {
+            return o.byteCodec.encode(initVector);
+        }
+
+        public String getInitVectorString(ByteCodec byteCodec) {
+            return byteCodec.encode(initVector);
+        }
+
+        @Override
+        protected byte[] doEncrypt(byte[] plaintext) {
+            return AesTools.cbcEncrypt(plaintext, initVector, o.padding, o.secretKey);
+        }
+
+        @Override
+        protected byte[] doDecrypt(byte[] ciphertext) {
+            return AesTools.cbcDecrypt(ciphertext, initVector, o.padding, o.secretKey);
+        }
+    }
+
+    /** AES/ECB加密解密类 **/
+    public static class EcbCipher extends BaseCipher {
+
+        private EcbCipher(Options options) {
+            super(options);
+        }
+
+        @Override
+        protected byte[] doEncrypt(byte[] plaintext) {
+            return AesTools.ecbEncrypt(plaintext, o.padding, o.secretKey);
+        }
+
+        @Override
+        protected byte[] doDecrypt(byte[] ciphertext) {
+            return AesTools.ecbDecrypt(ciphertext, o.padding, o.secretKey);
+        }
+    }
+
+    private static abstract class BaseCipher implements CipherService {
+
+        protected final Options o;
+
+        private BaseCipher(Options options) {
+            this.o = options;
+        }
+
+        protected abstract byte[] doEncrypt(byte[] plaintext);
+
+        protected abstract byte[] doDecrypt(byte[] ciphertext);
+
+        public Options options() {
+            return o;
+        }
+
+        /**
+         * 加密, 使用密钥加密
+         *
+         * @param plaintext 待加密的明文
+         * @return 加密后的密文
+         */
+        @Override
+        public String encrypt(String plaintext) {
+            VerifyTools.requireNonNull(plaintext, "plaintext");
+            String text = plaintext;
+            int randomChars = o.randomChars;
+            if (randomChars > 0) {
+                text = RandomTools.generateString(randomChars) + plaintext + RandomTools.generateString(randomChars);
+            }
+            byte[] input = o.textCodec.decode(text);
+            byte[] output = doEncrypt(input);
+            return o.byteCodec.encode(output);
+        }
+
+        /**
+         * 解密, 使用密钥解密
+         *
+         * @param ciphertext 待解密的密文
+         * @return 解密后的明文
+         */
+        @Override
+        public String decrypt(String ciphertext) {
+            VerifyTools.requireNonNull(ciphertext, "ciphertext");
+            byte[] input = o.byteCodec.decode(ciphertext);
+            byte[] output = doDecrypt(input);
+            String text = o.textCodec.encode(output);
+            int randomChars = o.randomChars;
+            return randomChars <= 0 ? text : text.substring(randomChars, text.length() - randomChars);
+        }
+
+        /**
+         * 加密, 使用密钥加密
+         *
+         * @param plaintext 待加密的明文
+         * @return 加密后的密文
+         */
+        @Override
+        public byte[] encrypt(byte[] plaintext) {
+            VerifyTools.requireNonNull(plaintext, "plaintext");
+            return doEncrypt(plaintext);
+        }
+
+        /**
+         * 解密, 使用密钥解密
+         *
+         * @param ciphertext 待解密的密文
+         * @return 解密后的明文
+         */
+        @Override
+        public byte[] decrypt(byte[] ciphertext) {
+            VerifyTools.requireNonNull(ciphertext, "ciphertext");
+            return doDecrypt(ciphertext);
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
index 81fe1e61cce89d27825c4f23d5f3905d3c02d79d..d3b197b86807509ba2400f7831c5e7c87faa5daa 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesEcbCipher.java
@@ -12,7 +12,9 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  *
  * @author zhaohuihua
  * @version 20200510
+ * @deprecated 改为 {@linkplain AesCipher}
  */
+@Deprecated
 public class AesEcbCipher {
 
     /** 输入输出文本的编解码方式 **/
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesTools.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesTools.java
index 122b2b8ff793dc561f4e2d3d13c8bee1f7186cf9..16f765d85ec773def743a7318ccfc7ca22d8bfe8 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/AesTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/AesTools.java
@@ -1,15 +1,19 @@
 package com.gitee.qdbp.tools.crypto;
 
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.Key;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.security.spec.InvalidParameterSpecException;
 import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.KeyGenerator;
 import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
@@ -24,35 +28,119 @@ public class AesTools {
 
     private final static String ALGORITHM = "AES";
     private final static String SHA1PRNG = "SHA1PRNG";
+
     private final static String ECB_CIPHER = "AES/ECB/PKCS5Padding";
+    private final static String CBC_CIPHER = "AES/CBC/PKCS5Padding";
 
     /**
-     * 数据加密
-     * 
+     * ECB数据加密
+     *
      * @param data 待加密的数据
      * @param key 密钥
      * @return 加密后的数据
      */
     public static byte[] ecbEncrypt(byte[] data, SecretKey key) {
-        Cipher cipher = getEcbCipherInstance(key, Cipher.ENCRYPT_MODE);
-        try {
-            return cipher.doFinal(data);
-        } catch (BadPaddingException e) {
-            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
-        } catch (IllegalBlockSizeException e) {
-            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
-        }
+        Cipher cipher = getEcbCipherInstance(ECB_CIPHER, key, Cipher.ENCRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * ECB数据加密
+     *
+     * @param data 待加密的数据
+     * @param padding 填充方式
+     * @param key 密钥
+     * @return 加密后的数据
+     */
+    public static byte[] ecbEncrypt(byte[] data, String padding, SecretKey key) {
+        String algorithm = padding == null ? ECB_CIPHER : "AES/ECB/" + padding;
+        Cipher cipher = getEcbCipherInstance(algorithm, key, Cipher.ENCRYPT_MODE);
+        return cipherDoFinal(data, cipher);
     }
 
     /**
-     * 数据解密
-     * 
+     * ECB数据解密
+     *
      * @param data 待解密的数据
      * @param key 密钥
      * @return 解密后的数据
      */
     public static byte[] ecbDecrypt(byte[] data, SecretKey key) {
-        Cipher cipher = getEcbCipherInstance(key, Cipher.DECRYPT_MODE);
+        Cipher cipher = getEcbCipherInstance(ECB_CIPHER, key, Cipher.DECRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * ECB数据解密
+     *
+     * @param data 待解密的数据
+     * @param padding 填充方式
+     * @param key 密钥
+     * @return 解密后的数据
+     */
+    public static byte[] ecbDecrypt(byte[] data, String padding, SecretKey key) {
+        String algorithm = padding == null ? ECB_CIPHER : "AES/ECB/" + padding;
+        Cipher cipher = getEcbCipherInstance(algorithm, key, Cipher.DECRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * CBC数据加密
+     *
+     * @param data 待加密的数据
+     * @param iv 初始向量
+     * @param key 密钥
+     * @return 加密后的数据
+     */
+    public static byte[] cbcEncrypt(byte[] data, byte[] iv, SecretKey key) {
+        Cipher cipher = getCbcCipherInstance(CBC_CIPHER, key, iv, Cipher.ENCRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * CBC数据加密
+     *
+     * @param data 待加密的数据
+     * @param iv 初始向量
+     * @param padding 填充方式
+     * @param key 密钥
+     * @return 加密后的数据
+     */
+    public static byte[] cbcEncrypt(byte[] data, byte[] iv, String padding, SecretKey key) {
+        String algorithm = padding == null ? CBC_CIPHER : "AES/CBC/" + padding;
+        Cipher cipher = getCbcCipherInstance(algorithm, key, iv, Cipher.ENCRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * CBC数据解密
+     *
+     * @param data 待解密的数据
+     * @param iv 初始向量
+     * @param key 密钥
+     * @return 解密后的数据
+     */
+    public static byte[] cbcDecrypt(byte[] data, byte[] iv, SecretKey key) {
+        Cipher cipher = getCbcCipherInstance(CBC_CIPHER, key, iv, Cipher.DECRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    /**
+     * CBC数据解密
+     *
+     * @param data 待解密的数据
+     * @param iv 初始向量
+     * @param key 密钥
+     * @param padding 填充方式
+     * @return 解密后的数据
+     */
+    public static byte[] cbcDecrypt(byte[] data, byte[] iv, String padding, SecretKey key) {
+        String algorithm = padding == null ? CBC_CIPHER : "AES/CBC/" + padding;
+        Cipher cipher = getCbcCipherInstance(algorithm, key, iv, Cipher.DECRYPT_MODE);
+        return cipherDoFinal(data, cipher);
+    }
+
+    private static byte[] cipherDoFinal(byte[] data, Cipher cipher) {
         try {
             return cipher.doFinal(data);
         } catch (BadPaddingException e) {
@@ -62,9 +150,43 @@ public class AesTools {
         }
     }
 
+    private static Cipher getEcbCipherInstance(String algorithm, Key secretKey, int cipherMode) {
+        try {
+            Cipher cipher = Cipher.getInstance(algorithm);
+            cipher.init(cipherMode, secretKey);
+            return cipher;
+        } catch (NoSuchAlgorithmException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (NoSuchPaddingException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (InvalidKeyException e) {
+            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
+        }
+    }
+
+    private static Cipher getCbcCipherInstance(String algorithm, Key secretKey, byte[] ivBytes, int cipherMode) {
+        try {
+            Cipher cipher = Cipher.getInstance(algorithm);
+            AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
+            parameters.init(new IvParameterSpec(ivBytes));
+            cipher.init(cipherMode, secretKey, parameters);
+            return cipher;
+        } catch (NoSuchAlgorithmException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (NoSuchPaddingException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (InvalidKeyException e) {
+            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        } catch (InvalidParameterSpecException e) {
+            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
+        }
+    }
+
     /**
      * 根据密钥数据生成密钥对象
-     * 
+     *
      * @param keyBytes 密钥数据
      * @param keyPadding key是否需要补全(如果key是32位的16进制字符串则不需要)
      * @return SecretKey对象
@@ -88,17 +210,23 @@ public class AesTools {
         }
     }
 
-    private static Cipher getEcbCipherInstance(Key secretKey, int cipherMode) {
+    /** 生成安全密钥 **/
+    public static byte[] generateSecretKey() {
         try {
-            Cipher cipher = Cipher.getInstance(ECB_CIPHER);
-            cipher.init(cipherMode, secretKey);
-            return cipher;
+            SecureRandom sr = SecureRandom.getInstanceStrong();
+            return sr.generateSeed(32);
         } catch (NoSuchAlgorithmException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
-        } catch (NoSuchPaddingException e) {
+        }
+    }
+
+    /** 生成随机CBC初始向量 **/
+    public static byte[] generateCbcInitVector() {
+        try {
+            SecureRandom sr = SecureRandom.getInstanceStrong();
+            return sr.generateSeed(16);
+        } catch (NoSuchAlgorithmException e) {
             throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
-        } catch (InvalidKeyException e) {
-            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, e);
         }
     }
 
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/DefaultRsaCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/DefaultRsaCipher.java
index 3ce07e437b931fe7fcadef9e3b8ef4d874f7561c..65c3620e4435e11f92d41a45bef861fa6a1783fa 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/DefaultRsaCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/DefaultRsaCipher.java
@@ -16,7 +16,7 @@ public class DefaultRsaCipher extends RsaCipher {
             "MP38SNHbnVWjkcky22qMDoFbcXapv3fJcfQk1Zjs1UWwKGapFDuW2Dos64PpBefHy4n4p1RdksgXNJmtT1KLBRaUUjnQ79HRrazdGcq2rrCiKjEEN56D2HCQouUXrpA6weSE6p5y3GAZ69bjBW8CbQPy6YNtf18Jor3iJZ5foJJhn6oauY2mxQXNJ4j2ajuFvsAWhNRLLubhFQrJj9HePWfLF1K4fGBFn6x3nPG38xvVmBRoetqT6tH4z8UgchUwr2Zx9rscvYfyShAwGiM8ncudkYzyD1UxUQiuuJJFWEv2iMDEARJ89d36x1C885gqrrFaYYC3bPMAxJ4gWuLQWYxqJjCx7vv4YU1TyigAwAr6MqgdkGMXZen4sGefLS7PkKyGmN16ecjS4qm9WEwhqoGq9vx85NFNqHAUMWd5Y1KgzsHBCZVhdTxXdkZUWrz8xCeVZMHZqAStJ46wKuDsgZX";
 
     public DefaultRsaCipher() {
-        super(DEFAULT_PUBLIC_KEY, DEFAULT_PRIVATE_KEY, Base58Codec.INSTANCE);
+        super(newOptions(DEFAULT_PUBLIC_KEY, DEFAULT_PRIVATE_KEY, Base58Codec.INSTANCE));
     }
 
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
index 2a51505cee709c7403b559113be8fd8791e9c854..80df0a6ff4f23c5d1f7e13ecd81b3dbdfccbb690 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaCipher.java
@@ -14,119 +14,323 @@ import com.gitee.qdbp.tools.utils.VerifyTools;
  */
 public class RsaCipher implements CipherService {
 
-    /** 输入输出文本的编解码方式 **/
-    private final TextCodec textCodec;
-    /** 输入输出Byte的编解码方式 **/
-    private final ByteCodec byteCodec;
-    /** 私钥 **/
-    private byte[] privateKey;
-    /** 公钥 **/
-    private byte[] publicKey;
+    private final Options o;
 
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    protected RsaCipher(Options options) {
+        this.o = options;
+    }
+
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(int keySize) {
         this(keySize, HexCodec.INSTANCE);
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(int keySize, ByteCodec byteCodec) {
-        this.textCodec = TextCodec.UTF8;
-        this.byteCodec = byteCodec;
-        KeyPair keyPair = RsaTools.generateKeyPair(keySize);
-        this.privateKey = keyPair.getPrivate().getEncoded();
-        this.publicKey = keyPair.getPublic().getEncoded();
+        this(newOptions(RsaTools.generateKeyPair(keySize), byteCodec));
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(String publicKey, String privateKey) {
         this(publicKey, privateKey, HexCodec.INSTANCE);
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(String publicKey, String privateKey, ByteCodec byteCodec) {
-        this.textCodec = TextCodec.UTF8;
-        this.byteCodec = byteCodec;
-        if (publicKey != null) {
-            this.publicKey = byteCodec.decode(publicKey);
-        }
-        if (privateKey != null) {
-            this.privateKey = byteCodec.decode(privateKey);
-        }
+        this(newOptions(publicKey, privateKey, byteCodec));
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(byte[] publicKey, byte[] privateKey) {
         this(publicKey, privateKey, HexCodec.INSTANCE);
     }
 
+    /**
+     * 构造函数
+     *
+     * @deprecated 改为 {@linkplain RsaCipher#builder()}
+     */
+    @Deprecated
     public RsaCipher(byte[] publicKey, byte[] privateKey, ByteCodec byteCodec) {
-        this.textCodec = TextCodec.UTF8;
-        this.byteCodec = byteCodec;
-        this.publicKey = publicKey;
-        this.privateKey = privateKey;
+        this(newOptions(publicKey, privateKey, byteCodec));
+    }
+
+    protected static Options newOptions(KeyPair keyPair, ByteCodec byteCodec) {
+        Options o = new Options();
+        o.byteCodec = byteCodec;
+        o.publicKey = keyPair.getPublic().getEncoded();
+        o.privateKey = keyPair.getPrivate().getEncoded();
+        return o;
+    }
+
+    protected static Options newOptions(String publicKey, String privateKey, ByteCodec byteCodec) {
+        Options o = new Options();
+        o.byteCodec = byteCodec;
+        o.publicKey = byteCodec.decode(publicKey);
+        o.privateKey = byteCodec.decode(privateKey);
+        return o;
+    }
+
+    private static Options newOptions(byte[] publicKey, byte[] privateKey, ByteCodec byteCodec) {
+        Options o = new Options();
+        o.byteCodec = byteCodec;
+        o.publicKey = publicKey;
+        o.privateKey = privateKey;
+        return o;
     }
 
-    /** 公钥 **/
+    public Options options() {
+        return o;
+    }
+
+    /**
+     * 获取公钥
+     *
+     * @deprecated 改为 options().getPublicKey() {@link Options#getPublicKey()}
+     */
+    @Deprecated
     public String getPublicKey() {
-        return publicKey == null ? null : byteCodec.encode(publicKey);
+        return o.publicKey == null ? null : o.byteCodec.encode(o.publicKey);
+    }
+
+    /**
+     * 获取私钥
+     *
+     * @deprecated 改为 options().getPrivateKey() {@link Options#getPrivateKey()}
+     */
+    @Deprecated
+    public String getPrivateKey() {
+        return o.privateKey == null ? null : o.byteCodec.encode(o.privateKey);
     }
 
     /**
      * 加密, 使用公钥加密
-     * 
+     *
      * @param plaintext 待加密的明文
      * @return 加密后的密文
      */
     @Override
     public String encrypt(String plaintext) {
         VerifyTools.requireNonNull(plaintext, "plaintext");
-        if (publicKey == null) {
+        if (o.publicKey == null) {
             throw new IllegalStateException("PublicKey not configured");
         }
-        byte[] input = textCodec.decode(plaintext);
-        byte[] output = RsaTools.encrypt(input, publicKey);
-        return byteCodec.encode(output);
+        byte[] input = o.textCodec.decode(plaintext);
+        byte[] output = RsaTools.encrypt(input, o.publicKey);
+        return o.byteCodec.encode(output);
     }
 
     /**
      * 加密, 使用公钥加密
-     * 
+     *
      * @param plaintext 待加密的明文
      * @return 加密后的密文
      */
     @Override
     public byte[] encrypt(byte[] plaintext) {
         VerifyTools.requireNonNull(plaintext, "plaintext");
-        if (publicKey == null) {
+        if (o.publicKey == null) {
             throw new IllegalStateException("PublicKey not configured");
         }
-        return RsaTools.encrypt(plaintext, publicKey);
+        return RsaTools.encrypt(plaintext, o.publicKey);
     }
 
     /**
      * 解密, 使用私钥解密
-     * 
+     *
      * @param ciphertext 待解密的密文
      * @return 解密后的明文
      */
     @Override
     public String decrypt(String ciphertext) {
         VerifyTools.requireNonNull(ciphertext, "ciphertext");
-        if (privateKey == null) {
+        if (o.privateKey == null) {
             throw new IllegalStateException("PrivateKey not configured");
         }
-        byte[] input = byteCodec.decode(ciphertext);
-        byte[] output = RsaTools.decrypt(input, privateKey);
-        return textCodec.encode(output);
+        byte[] input = o.byteCodec.decode(ciphertext);
+        byte[] output = RsaTools.decrypt(input, o.privateKey);
+        return o.textCodec.encode(output);
     }
 
     /**
      * 解密, 使用私钥解密
-     * 
+     *
      * @param ciphertext 待解密的密文
      * @return 解密后的明文
      */
     @Override
     public byte[] decrypt(byte[] ciphertext) {
         VerifyTools.requireNonNull(ciphertext, "ciphertext");
-        if (privateKey == null) {
+        if (o.privateKey == null) {
             throw new IllegalStateException("PrivateKey not configured");
         }
-        return RsaTools.decrypt(ciphertext, privateKey);
+        return RsaTools.decrypt(ciphertext, o.privateKey);
+    }
+
+
+    public static class Options {
+        /** 输入输出文本的编解码方式 **/
+        private TextCodec textCodec = TextCodec.UTF8;
+        /** 输入输出Byte的编解码方式 **/
+        private ByteCodec byteCodec = HexCodec.INSTANCE;
+        /** 私钥 **/
+        private byte[] privateKey;
+        /** 公钥 **/
+        private byte[] publicKey;
+
+        /** 输入输出文本的编解码方式 **/
+        public TextCodec getTextCodec() {
+            return textCodec;
+        }
+
+        /** 输入输出Byte的编解码方式 **/
+        public ByteCodec getByteCodec() {
+            return byteCodec;
+        }
+
+        /** 公钥 **/
+        public byte[] getPublicBytes() {
+            return publicKey;
+        }
+
+        /** 私钥 **/
+        public byte[] getPrivateBytes() {
+            return privateKey;
+        }
+
+        /** 公钥 **/
+        public String getPublicKey() {
+            return publicKey == null ? null : byteCodec.encode(publicKey);
+        }
+
+        /** 私钥 **/
+        public String getPrivateKey() {
+            return privateKey == null ? null : byteCodec.encode(privateKey);
+        }
+
+        /** 公钥 **/
+        public String getPublicKey(ByteCodec byteCodec) {
+            return publicKey == null ? null : byteCodec.encode(publicKey);
+        }
+
+        /** 私钥 **/
+        public String getPrivateKey(ByteCodec byteCodec) {
+            return privateKey == null ? null : byteCodec.encode(privateKey);
+        }
+    }
+
+    public static class Builder {
+
+        private final Options o = new Options();
+
+        /** 输入输出文本的编解码方式 **/
+        public Builder textCodec(TextCodec textCodec) {
+            VerifyTools.requireNonNull(textCodec, "textCodec");
+            this.o.textCodec = textCodec;
+            return this;
+        }
+
+        /** 输入输出Byte的编解码方式 **/
+        public Builder byteCodec(ByteCodec byteCodec) {
+            VerifyTools.requireNonNull(byteCodec, "byteCodec");
+            this.o.byteCodec = byteCodec;
+            return this;
+        }
+
+        /** 生成1024位随机密钥 **/
+        public Builder generateKey() {
+            KeyPair keyPair = RsaTools.generateKeyPair();
+            this.o.privateKey = keyPair.getPrivate().getEncoded();
+            this.o.publicKey = keyPair.getPublic().getEncoded();
+            return this;
+        }
+
+        /** 生成指定长度的随机密钥 **/
+        public Builder generateKey(int keySize) {
+            KeyPair keyPair = RsaTools.generateKeyPair(keySize);
+            this.o.privateKey = keyPair.getPrivate().getEncoded();
+            this.o.publicKey = keyPair.getPublic().getEncoded();
+            return this;
+        }
+
+        /** 密钥字符串 **/
+        public Builder secretKey(byte[] publicKey, byte[] privateKey) {
+            VerifyTools.requireNonNull(publicKey, "publicKey");
+            VerifyTools.requireNonNull(privateKey, "privateKey");
+            this.o.publicKey = publicKey;
+            this.o.privateKey = privateKey;
+            return this;
+        }
+
+        /** 密钥字符串 **/
+        public Builder secretKey(String publicKey, String privateKey, ByteCodec keyCodec) {
+            VerifyTools.requireNotBlank(publicKey, "publicKey");
+            VerifyTools.requireNotBlank(privateKey, "privateKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            return secretKey(keyCodec.decode(publicKey), keyCodec.decode(privateKey));
+        }
+
+        /** 公钥字符串 (只做加密可以只设公钥) **/
+        public Builder publicKey(byte[] publicKey) {
+            VerifyTools.requireNonNull(publicKey, "publicKey");
+            this.o.publicKey = publicKey;
+            return this;
+        }
+
+        /** 公钥字符串 (只做加密可以只设公钥) **/
+        public Builder publicKey(String publicKey, ByteCodec keyCodec) {
+            VerifyTools.requireNotBlank(publicKey, "publicKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            this.o.publicKey = keyCodec.decode(publicKey);
+            return this;
+        }
+
+        /** 私钥字符串 (只做解密可以只设私钥) **/
+        public Builder privateKey(byte[] privateKey) {
+            VerifyTools.requireNotBlank(privateKey, "privateKey");
+            this.o.privateKey = privateKey;
+            return this;
+        }
+
+        /** 私钥字符串 (只做解密可以只设私钥) **/
+        public Builder privateKey(String privateKey, ByteCodec keyCodec) {
+            VerifyTools.requireNotBlank(privateKey, "privateKey");
+            VerifyTools.requireNonNull(keyCodec, "keyCodec");
+            this.o.privateKey = keyCodec.decode(privateKey);
+            return this;
+        }
+
+        public RsaCipher build() {
+            return new RsaCipher(o);
+        }
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaPool.java b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaPool.java
index 51615cf6f77b2ff8050167ad8b45b4587788d5d5..3714274a061f1448736ba08bd3b70d6f2f804596 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaPool.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/crypto/RsaPool.java
@@ -13,13 +13,14 @@ public class RsaPool {
     private static final int KEY_SIZE_DEFAULT = 1024;
     public static final RsaPool instance = new RsaPool();
     /** 主实例 **/
-    private RsaCipher majorCipher = new RsaCipher(KEY_SIZE_DEFAULT, new Base58Codec());
+    private RsaCipher majorCipher = RsaCipher.builder()
+            .byteCodec(Base58Codec.INSTANCE).generateKey(KEY_SIZE_DEFAULT).build();
     /** 备用实例, 更换主实例后, 原先的主实例降为备用实例继续服务一段时间 **/
     private RsaCipher minorCipher = null;
 
     /** 获取当前公钥 **/
     public String getPublicKey() {
-        return majorCipher.getPublicKey();
+        return majorCipher.options().getPublicKey();
     }
 
     /** 使用私钥解密 **/
diff --git a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
index f5bbe0485c9f8f25b7a0409556239c060a6f44f1..9b3f9258c7c94c7b58db7b486e2f8c68ea2231d9 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/files/PathTools.java
@@ -8,12 +8,14 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
 import java.net.JarURLConnection;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLConnection;
+import java.net.URLDecoder;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -364,6 +366,23 @@ public abstract class PathTools {
         }
     }
 
+    /**
+     * 扫描class资源
+     *
+     * @param packages 包名前缀 (包名不需要输全, com.gitee也可以)
+     * @param className 类名 (如StringTools)
+     * @return 路径列表
+     */
+    public static List<String> scanClassResources(String packages, String className) {
+        String folder = StringTools.replace(packages, ".", "/");
+        List<URL> urls = PathTools.scanResources(folder, className);
+        List<String> result = new ArrayList<>();
+        for (URL url : urls) {
+            result.add(getClasspathRelativePath(url.toString()));
+        }
+        return result;
+    }
+
     /**
      * 按名称扫描资源, 不支持通配符
      * 
@@ -876,6 +895,48 @@ public abstract class PathTools {
         return toUriPath(root);
     }
 
+    /**
+     * 获取指定类所在的classpath
+     *
+     * @param clazz 指定类
+     * @return classpath
+     */
+    public static String getClassPath(Class<?> clazz) {
+        // 获取与JAR包自身所在的文件夹
+        // project-path/target/classes/
+        URL url = clazz.getResource("/");
+        if (url != null) {
+            return PathTools.toUriPath(url);
+        } else { // jar包中获取为空
+            // project-path/target/classes/com/gitee/qdbp/.../
+            // jar:file:/F:/project-path/xxx.jar!/com/gitee/qdbp/.../
+            url = clazz.getResource("");
+            assert url != null;
+            String p = PathTools.toUriPath(url);
+            p = StringTools.removePrefix(p, "jar://");
+            p = StringTools.removePrefix(p, "jar:");
+            p = StringTools.removePrefix(p, "file:");
+            int index = p.indexOf('!');
+            if (index > 0) {
+                p = PathTools.removeFileName(p.substring(0, index));
+            }
+            if ((p.length() > 2) && (p.charAt(2) == ':')) {
+                // "/c:/foo/" --> "c:/foo/"
+                p = p.substring(1);
+            }
+            if (!new File(p).exists() && p.contains("%")) {
+                try {
+                    p = URLDecoder.decode(p, "UTF-8");
+                    if (!new File(p).exists()) {
+                        p = URLDecoder.decode(p, "GBK");
+                    }
+                } catch (UnsupportedEncodingException ignore) {
+                }
+            }
+            return p;
+        }
+    }
+
     /**
      * 计算输出文件夹路径<br>
      * 如果root不是文件, 则一律返回classpath对应文件夹
@@ -987,7 +1048,7 @@ public abstract class PathTools {
             }
             if (!endsWithSeparator(buffer) && !startsWithSeparator(path)) {
                 buffer.append(SLASH).append(path);
-            } else if (endsWithSeparator(folder) && startsWithSeparator(path)) {
+            } else if (endsWithSeparator(buffer) && startsWithSeparator(path)) {
                 buffer.append(path.substring(1));
             } else {
                 buffer.append(path);
@@ -1279,33 +1340,83 @@ public abstract class PathTools {
     }
 
     /**
-     * 获取classpath相对路径
-     * 
+     * 获取classpath相对路径<br>
+     * jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties<br>
+     * 都返回 settings/sqls/xxx.properties<br>
+     *
      * @param url URL路径
      * @return 相对路径
      */
     public static String getClasspathRelativePath(URL url) {
-        return getClasspathRelativePath(url.toString());
+        return doGetClasspathRelativePath(url.toString(), false);
     }
 
     /**
-     * 获取classpath相对路径
-     * 
+     * 获取classpath相对路径<br>
+     * jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties<br>
+     * 都返回 settings/sqls/xxx.properties<br>
+     *
      * @param urlPath URL路径
      * @return 相对路径
      */
     public static String getClasspathRelativePath(String urlPath) {
-        // jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties
-        // file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties
+        return doGetClasspathRelativePath(urlPath, false);
+    }
+
+    /**
+     * 获取classpath相对路径<br>
+     * jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * 返回 xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties<br>
+     * 返回 classes/settings/sqls/xxx.properties
+     *
+     * @param url URL路径
+     * @return 相对路径
+     */
+    public static String getJarRelativePath(URL url) {
+        return doGetClasspathRelativePath(url.toString(), true);
+    }
+
+    /**
+     * 获取classpath相对路径<br>
+     * jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * 返回 xxx-1.0.0.jar!/settings/sqls/xxx.properties<br>
+     * file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties<br>
+     * 返回 classes/settings/sqls/xxx.properties
+     *
+     * @param urlPath URL路径
+     * @return 相对路径
+     */
+    public static String getJarRelativePath(String urlPath) {
+        return doGetClasspathRelativePath(urlPath, true);
+    }
+
+    private static String doGetClasspathRelativePath(String urlPath, boolean includeJarName) {
         String lowerCase = urlPath.toLowerCase();
         String jarFlag = ".jar!/";
         int jarIndex = lowerCase.indexOf(jarFlag);
         if (jarIndex > 0) {
-            return urlPath.substring(jarIndex + jarFlag.length());
+            if (!includeJarName) {
+                return urlPath.substring(jarIndex + jarFlag.length());
+            } else {
+                int slashIndex = lowerCase.lastIndexOf('/', jarIndex);
+                if (slashIndex >= 0) {
+                    return urlPath.substring(slashIndex + 1);
+                } else {
+                    return urlPath;
+                }
+            }
         }
-        int classesIndex = lowerCase.indexOf("/classes/");
-        if (classesIndex > 0) {
-            return urlPath.substring(classesIndex + "/classes/".length());
+        String classFlag = "/classes/";
+        int classesIndex = lowerCase.lastIndexOf(classFlag);
+        if (classesIndex >= 0) {
+            if (!includeJarName) {
+                return urlPath.substring(classesIndex + classFlag.length());
+            } else {
+                return urlPath.substring(classesIndex + 1);
+            }
         }
         return urlPath;
     }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/files/ZipTools.java b/able/src/main/java/com/gitee/qdbp/tools/files/ZipTools.java
index d3a217e15ee116e4a61efc2796c82094b321dca3..6205e3f2ffaafa803bdccd5b7b544cb2d4f34c76 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/files/ZipTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/files/ZipTools.java
@@ -37,13 +37,24 @@ public class ZipTools {
      * @param saveFolder 保存文件的路径
      */
     public static void decompression(String srcPath, String saveFolder) throws IOException {
+        decompression(srcPath, saveFolder, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * ZIP文件解压
+     *
+     * @param srcPath 源文件
+     * @param saveFolder 保存文件的路径
+     * @param charset 编码格式
+     */
+    public static void decompression(String srcPath, String saveFolder, Charset charset) throws IOException {
         File srcFile = new File(srcPath);
         if (!srcFile.exists()) { // 判断源文件是否存在
             throw new ResourceNotFoundException(srcPath);
         }
         String fileName = PathTools.removeExtension(new File(srcPath).getName()) + '/';
         // 开始解压
-        try (ZipFile zipFile = new ZipFile(srcFile)) {
+        try (ZipFile zipFile = new ZipFile(srcFile, charset)) {
             Enumeration<?> entries = zipFile.entries();
             while (entries.hasMoreElements()) {
                 ZipEntry entry = (ZipEntry) entries.nextElement();
diff --git a/able/src/main/java/com/gitee/qdbp/tools/parse/ParseUtils.java b/able/src/main/java/com/gitee/qdbp/tools/parse/ParseUtils.java
index 1adcb11c86d82938fef7a00fd6ed40aec204a44a..8425197fd2ed6ed9d06ed15ecf0f42c7eba8fc6c 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/parse/ParseUtils.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/parse/ParseUtils.java
@@ -36,7 +36,7 @@ class ParseUtils {
         if (string == null) {
             return null;
         }
-        string = StringTools.removeLeftRight(string.trim(), mark);
+        string = StringTools.trimLeftRight(string.trim(), mark);
         StringBuilder buffer = new StringBuilder();
         for (int i = 0, z = string.length() - 1; i <= z; i++) {
             char c = string.charAt(i);
diff --git a/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java b/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
index f71f832167c3b18a084015aa82aa3755e2ae3acd..e0c576ee3dd0881be78366b37bfdbb22883e15ca 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/parse/StringAccess.java
@@ -34,11 +34,11 @@ public class StringAccess {
 
     /**
      * 构造函数
-     * 
+     *
      * @param string 源字符串
      * @param index 开始位置
      * @param multiline 是否支持多行模式: false=最多读取至行尾; true=最多读取至文本结束<br>
-     *            如 读取括号中的内容, (1\n2) 在多行模式下正确; 在单行模式下会报错(因为读取到行尾都没找到结束的括号)
+     * 如 读取括号中的内容, (1\n2) 在多行模式下正确; 在单行模式下会报错(因为读取到行尾都没找到结束的括号)
      */
     public StringAccess(String string, int index, boolean multiline) {
         this.string = string;
@@ -52,7 +52,7 @@ public class StringAccess {
      * 最后一次操作是否成功<br>
      * 遇到结尾导致没读取到内容也算成功(因为结尾可以通过isReadable判断)<br>
      * 只有想读数字却不是数字这种类型的失败才算失败<br>
-     * 
+     *
      * @return 是否成功
      */
     public boolean isLastReadSuccess() {
@@ -69,7 +69,7 @@ public class StringAccess {
 
     /**
      * 获取当前位置
-     * 
+     *
      * @return 当前位置
      */
     public int readerIndex() {
@@ -89,7 +89,7 @@ public class StringAccess {
 
     /**
      * 设置当前位置
-     * 
+     *
      * @param index 指定位置
      * @return 当前对象
      * @throws IndexOutOfBoundsException 位置超出源字符串范围
@@ -122,10 +122,24 @@ public class StringAccess {
         }
     }
 
+    /**
+     * 跳过1个字符<br>
+     * 即使已到末尾没得读取, 也会成功<br>
+     * 所有skip操作lastReadSuccess=true, 在这点上与read/peek等读取操作不一样
+     *
+     * @return 返回对象自身用于链式操作
+     */
     public StringAccess skipChar() {
         return skipChars(1);
     }
 
+    /**
+     * 跳过n个字符<br>
+     * 即使到末尾不够数量, 也会成功, 此时index将停留在最后<br>
+     * 所有skip操作lastReadSuccess=true, 在这点上与read/peek等读取操作不一样
+     *
+     * @return 返回对象自身用于链式操作
+     */
     public StringAccess skipChars(int size) {
         this.index = Math.min(index + size, length);
         this.lastReadSuccess = true;
@@ -135,7 +149,7 @@ public class StringAccess {
     /**
      * 读取几个字符<br>
      * 如果不够读取的数量, 将设置lastReadSuccess=false并返回null
-     * 
+     *
      * @param size 期望读取的数量
      * @return 字符串
      */
@@ -157,8 +171,8 @@ public class StringAccess {
 
     /**
      * 查看几个字符<br>
-     * 如果不够查看的数量, 将返回null
-     * 
+     * 如果不够查看的数量, 将设置lastReadSuccess=false并返回null
+     *
      * @param size 期望查看的数量
      * @return 字符串
      */
@@ -179,7 +193,7 @@ public class StringAccess {
     /**
      * 读取连续的指定字符<br>
      * 如果一个字符都没有读取到, 将设置lastReadSuccess=false并返回null
-     * 
+     *
      * @param chars 指定字符
      * @return 字符串
      */
@@ -204,12 +218,22 @@ public class StringAccess {
         }
     }
 
-    /** 跳过空白字符 (空格/TAB/换行) **/
+    /**
+     * 跳过空白字符 (空格/TAB/换行)<br>
+     * 所有skip操作lastReadSuccess=true, 在这点上与read/peek等读取操作不一样
+     *
+     * @return 返回对象自身用于链式操作
+     */
     public StringAccess skipWhitespace() {
         return this.doSkipWhitespace(true);
     }
 
-    /** 跳过空格和TAB字符 **/
+    /**
+     * 跳过空格和TAB字符 (不会跳过换行符)<br>
+     * 所有skip操作lastReadSuccess=true, 在这点上与read/peek等读取操作不一样
+     *
+     * @return 返回对象自身用于链式操作
+     */
     public StringAccess skipSpaceChars() {
         return this.doSkipWhitespace(false);
     }
@@ -315,6 +339,29 @@ public class StringAccess {
         return originIndex == index ? null : string.substring(originIndex, index);
     }
 
+    /** 读取Ascii字符 **/
+    public String readAscii() {
+        if (index >= length) {
+            this.lastReadSuccess = true;
+            return null;
+        }
+        // 记录原始位置
+        int originIndex = index;
+        // 遍历获取结束位置
+        for (; index < length; index++) {
+            char c = string.charAt(index);
+            if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
+                continue;
+            } else {
+                break;
+            }
+        }
+
+        // 想要读取单词, 却不是单词, 则为读取失败
+        this.lastReadSuccess = index > originIndex;
+        return originIndex == index ? null : string.substring(originIndex, index);
+    }
+
     /** 读取单词 (数字字母下划线美元符) **/
     public String readWord() {
         if (index >= length) {
@@ -342,7 +389,7 @@ public class StringAccess {
      * 从当前位置开始读取, 直到遇到指定字符<br>
      * 返回字符串不会包含指定字符<br>
      * 如果到最后也没遇到指定字符, 将还原位置/设置lastReadSuccess=false/返回null<br>
-     * 
+     *
      * @param chars 指定字符
      * @return 子字符串
      */
@@ -381,7 +428,7 @@ public class StringAccess {
      * 从当前位置开始读取, 直到遇到指定字符串<br>
      * 返回字符串不会包含指定字符串<br>
      * 如果到最后也没遇到指定字符串, 将还原位置/设置lastReadSuccess=false/返回null<br>
-     * 
+     *
      * @param strings 指定字符串
      * @return 子字符串
      */
@@ -399,7 +446,6 @@ public class StringAccess {
             }
             char c = string.charAt(newIndex);
             if (!multiline && (c == '\r' || c == '\n')) {
-                found = false;
                 break; // 单行模式下遇到换行符, 也是结束
             } else {
                 newIndex++;
@@ -421,7 +467,7 @@ public class StringAccess {
      * 从当前位置开始读取, 直到行尾<br>
      * 返回字符串不会包含指定字符<br>
      * 如果到最后也没遇到换行符, 将读取到最后<br>
-     * 
+     *
      * @return 子字符串
      */
     public String readToLineEnd() {
@@ -453,7 +499,7 @@ public class StringAccess {
         }
         return false;
     }
-    
+
     private static boolean isStartsWith(String string, int index, String... prefixes) {
         for (String prefix : prefixes) {
             if (index + prefix.length() > string.length()) {
@@ -498,7 +544,7 @@ public class StringAccess {
 
     /**
      * 查看从指定位置到当前位置的字符串
-     * 
+     *
      * @param startIndex 开始位置
      * @return 子字符串
      * @throws IndexOutOfBoundsException 位置超出源字符串范围
@@ -513,7 +559,7 @@ public class StringAccess {
 
     /**
      * 查看子字符串
-     * 
+     *
      * @param startIndex 开始位置
      * @param endIndex 结束位置
      * @return 子字符串
@@ -539,7 +585,7 @@ public class StringAccess {
      * 返回内容不会包含外层括号<br>
      * 如果没有找到匹配的括号, 将还原位置/设置lastReadSuccess=false/返回null<br>
      * 支持括号嵌套, 如 new StringAccess("(1/(a+b))").readInBracket('(', ')')将返回1/(a+b)
-     * 
+     *
      * @param leftBracket 左括号
      * @param rightBracket 右括号
      * @return 括号中的内容
@@ -559,7 +605,7 @@ public class StringAccess {
             return null;
         }
         // userName (pinyin)
-        char[] stops = multiline ? new char[] { ' ', '\t', '\r', '\n', ',' } : new char[] { ' ', '\t', ',' };
+        char[] stops = multiline ? new char[] {' ', '\t', '\r', '\n', ','} : new char[] {' ', '\t', ','};
         String result = readUniterm(stops); // (pinyin)
         if (!isLastReadSuccess() || result.charAt(0) != leftBracket
                 || result.charAt(result.length() - 1) != rightBracket) {
@@ -581,9 +627,9 @@ public class StringAccess {
      * Math.round(user.height), Math.round( Math.abs(user.height) )<br>
      * StringTools.removeChars('8,000.00(元)', ',')<br>
      * {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] }
-     * 
+     *
      * @param stopChars 停止符<br>
-     *            如 readUniterm(' ', '\t', '\r', '\n', ';')表示读取至空白或分号结束
+     * 如 readUniterm(' ', '\t', '\r', '\n', ';')表示读取至空白或分号结束
      * @return 单项内容
      */
     public String readUniterm(char... stopChars) {
@@ -624,9 +670,9 @@ public class StringAccess {
      * Math.round(user.height), Math.round( Math.abs(user.height) )<br>
      * StringTools.removeChars('8,000.00(元)', ',')<br>
      * {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] }
-     * 
+     *
      * @param separators 分隔符<br>
-     *            如 readUniterms(' ', '\t', '\r', '\n', ';')表示读取至空白或分号结束
+     * 如 readUniterms(' ', '\t', '\r', '\n', ';')表示读取至空白或分号结束
      * @return 多个单项内容列表
      */
     public List<String> readUniterms(char... separators) {
@@ -704,7 +750,7 @@ public class StringAccess {
      * ( Math.round(user.height), Math.round( Math.abs(user.height) ) )<br>
      * ( StringTools.removeChars('8,000.00(元)', ',') )<br>
      * ( {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] } )
-     * 
+     *
      * @return 方法参数列表
      */
     public List<String> readMethodParams() {
@@ -720,7 +766,7 @@ public class StringAccess {
      * ( Math.round(user.height), Math.round( Math.abs(user.height) ) )<br>
      * ( StringTools.removeChars('8,000.00(元)', ',') )<br>
      * ( {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] } )
-     * 
+     *
      * @param separators 分隔符
      * @return 方法参数列表
      */
diff --git a/able/src/main/java/com/gitee/qdbp/tools/parse/StringParser.java b/able/src/main/java/com/gitee/qdbp/tools/parse/StringParser.java
index f7bcb6590058a20215a15aa24a1a86e1e9e98235..a51f1377faf611bac933ee70e4f8c7abbc3871aa 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/parse/StringParser.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/parse/StringParser.java
@@ -10,6 +10,22 @@ import java.util.List;
  */
 public class StringParser {
 
+    /**
+     * 解析方法参数, 支持的参数形式如下:<br>
+     * ( user.name, 'xxx', 'xxx,yyy', ['aaa', 'bbb'] )<br>
+     * ( user.name + ', hello!', 'Hello ' + user.name + ', welcome!' )<br>
+     * ( 'Let\'s go', "Let's go", '8,000.00', '8,000.00(元)' )<br>
+     * ( Math.round(user.height), Math.round( Math.abs(user.height) ) )<br>
+     * ( StringTools.removeChars('8,000.00(元)', ',') )<br>
+     * ( {'a':'1','b':'2'} , { a:1, b:'xxx,yyy', c:[ {c1:'3'}, {c2:['aaa', 'bbb']} ] } )
+     *
+     * @param string 字符串
+     * @return 解析结果
+     */
+    public static MethodParamItems parseMethodParams(String string) {
+        return MethodParamParser.parse(string, 0);
+    }
+
     /**
      * 解析方法参数, 支持的参数形式如下:<br>
      * ( user.name, 'xxx', 'xxx,yyy', ['aaa', 'bbb'] )<br>
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/AssertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/AssertTools.java
index bf44125032ec559e4129074f52af8a1caab8a497..ab1cb6e6f6ce9664432fc4c6159b522aa69086b7 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/AssertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/AssertTools.java
@@ -119,7 +119,7 @@ public class AssertTools {
         for (int i = 0; i < aLines; i++) {
             String aString = actualLines[i];
             String eString = expectLines[i];
-            if (assertTextEquals(aString, eString)) {
+            if (checkTextEquals(aString, eString)) {
                 continue;
             }
 
@@ -185,7 +185,7 @@ public class AssertTools {
         assertTextLinesEquals(actual, eContent, desc);
     }
 
-    private static boolean assertTextEquals(String aString, String eString) {
+    protected static boolean checkTextEquals(String aString, String eString) {
         if (aString.length() != eString.length()) {
             return false;
         }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
index 3c9c9ad1a04eec4a8c0f6c3801253c5c6a4cc54f..a6e6af24f86d4cf766773bc52b1b8b79e36f6824 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ConvertTools.java
@@ -5,6 +5,12 @@ import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.text.DecimalFormat;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -14,6 +20,7 @@ import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -34,36 +41,20 @@ public class ConvertTools {
 
     /**
      * 将对象解析为列表
-     * 
-     * @param object 待解析的对象
-     * @return 解析后的列表, 如果object=null则返回null
-     * @since 4.1
-     */
-    public static List<Object> parseList(Object object) {
-        if (object == null) {
-            return null;
-        } else {
-            return parseListWithoutNull(object);
-        }
-    }
-
-    /**
-     * 将对象解析为列表
-     * 
+     *
      * @param object 待解析的对象
-     * @return 解析后的列表, 如果object=null则返回空列表
+     * @return 解析后的列表, 如果object=null将返回空列表
      * @since 4.1.0
      */
-    public static List<Object> parseListIfNullToEmpty(Object object) {
+    public static List<Object> parseList(Object object) {
         if (object == null) {
             return new ArrayList<>();
-        } else {
-            return parseListWithoutNull(object);
         }
-    }
-
-    private static List<Object> parseListWithoutNull(Object object) {
-        if (object instanceof Collection) {
+        if (object instanceof List) {
+            @SuppressWarnings("unchecked")
+            List<Object> list = (List<Object>) object;
+            return list;
+        } else if (object instanceof Collection) {
             return new ArrayList<>((Collection<?>) object);
         } else if (object.getClass() == int[].class) {
             List<Object> list = new ArrayList<>();
@@ -115,9 +106,8 @@ public class ConvertTools {
             return list;
         } else if (object.getClass().isArray()) {
             return new ArrayList<>(Arrays.asList((Object[]) object));
-        } else if (object instanceof Map) {
-            Map<?, ?> map = (Map<?, ?>) object;
-            return new ArrayList<>(map.values());
+        } else if (object instanceof Map && isListLikeness((Map<?, ?>) object)) {
+            return new ArrayList<>(((Map<?, ?>) object).values());
         } else if (object instanceof Iterable) {
             List<Object> values = new ArrayList<>();
             Iterable<?> iterable = (Iterable<?>) object;
@@ -144,21 +134,54 @@ public class ConvertTools {
         }
     }
 
+    // 所有key是从0开始的连续整数, 就判定为类似数组
+    // Yaml的数组有时会解析为key为序号的map, 因此加一个特殊处理
+    private static boolean isListLikeness(Map<?, ?> map) {
+        if (map.isEmpty() || !(map instanceof LinkedHashMap)) {
+            return false;
+        }
+        int index = 0;
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            Object key = entry.getKey();
+            if (key instanceof Integer) {
+                if (key.equals(index++)) {
+                    continue;
+                }
+            } else if (key instanceof String && StringTools.isDigit((String) key)) {
+                if (Integer.parseInt((String) key) == index++) {
+                    continue;
+                }
+            }
+            return false;
+        }
+        // 所有key是从0开始的连续整数
+        return true;
+    }
+
+    /**
+     * 将对象解析为列表
+     *
+     * @param object 待解析的对象
+     * @return 解析后的列表, 如果object=null则返回空列表
+     * @since 4.1.0
+     * @deprecated 改为 {@linkplain #parseList(Object)}
+     */
+    @Deprecated
+    public static List<Object> parseListIfNullToEmpty(Object object) {
+        return parseList(object);
+    }
+
     /**
      * List转数组
-     * 
+     *
      * @param list 待转换的List
      * @param clazz 目标类型
      * @param <T> 目标类型
      * @param <C> List内容的类型
-     * @return 转换后的数组, 如果list=null则返回null
+     * @return 转换后的数组, 如果list=null将返回T[0]
      */
     public static <T, C extends T> T[] toArray(Collection<C> list, Class<T> clazz) {
-        if (list == null) {
-            return null;
-        }
-
-        int size = list.size();
+        int size = list == null ? 0 : list.size();
         @SuppressWarnings("unchecked")
         T[] copy = clazz == Object[].class ? (T[]) new Object[size] : (T[]) Array.newInstance(clazz, size);
         return size == 0 ? copy : list.toArray(copy);
@@ -166,35 +189,34 @@ public class ConvertTools {
 
     /**
      * List转数组
-     * 
+     *
      * @param list 待转换的List
      * @param clazz 目标类型
      * @param <T> 目标类型
      * @param <C> List内容的类型
      * @return 转换后的数组, 如果list=null则返回T[0]
+     * @deprecated 改为 {@linkplain #toArray(Collection, Class)}
      */
+    @Deprecated
     public static <T, C extends T> T[] toArrayIfNullToEmpty(Collection<C> list, Class<T> clazz) {
-        int size = list == null ? 0 : list.size();
-        @SuppressWarnings("unchecked")
-        T[] copy = clazz == Object[].class ? (T[]) new Object[size] : (T[]) Array.newInstance(clazz, size);
-        return size == 0 ? copy : list.toArray(copy);
+        return toArray(list, clazz);
     }
 
     /**
      * 数组转List
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
-     * @return 转换后的List, 如果array=null则返回null
+     * @return 转换后的List, 如果array=null将返回空List
      */
     @SafeVarargs
     public static <T, C extends T> List<T> toList(C... array) {
         if (array == null) {
-            return null;
+            return new ArrayList<>();
         } else {
             // JDK1.7必须强转, JDK1.8不需要
-            @SuppressWarnings("unchecked")
+            @SuppressWarnings("all")
             List<T> temp = (List<T>) new ArrayList<>(Arrays.asList(array));
             return temp;
         }
@@ -202,37 +224,32 @@ public class ConvertTools {
 
     /**
      * 数组转List
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
-     * @return 转换后的List, 如果array=null则返回EmptyList
+     * @return 转换后的List, 如果array=null则返回空List
+     * @deprecated 改为 {@linkplain #toList(Object...)}
      */
+    @Deprecated
     @SafeVarargs
     public static <T, C extends T> List<T> toListIfNullToEmpty(C... array) {
-        if (array == null) {
-            return new ArrayList<>();
-        } else {
-            // JDK1.7必须强转, JDK1.8不需要
-            @SuppressWarnings("unchecked")
-            List<T> temp = (List<T>) new ArrayList<>(Arrays.asList(array));
-            return temp;
-        }
+        return toList(array);
     }
 
     /**
      * 数组转Set
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
-     * @return 转换后的Set, 如果array=null则返回null
+     * @return 转换后的Set, 如果array=null将返回空Set
      */
     @SafeVarargs
     @SuppressWarnings("unchecked")
     public static <T, C extends T> Set<T> toSet(C... array) {
         if (array == null) {
-            return null;
+            return new HashSet<>();
         } else {
             Set<C> list = new HashSet<>(Arrays.asList(array));
             // JDK1.7必须强转, JDK1.8不需要
@@ -242,37 +259,34 @@ public class ConvertTools {
 
     /**
      * 数组转Set
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
-     * @return 转换后的Set, 如果array=null则返回EmptySet
+     * @return 转换后的Set, 如果array=null则返回空Set
+     * @deprecated 改为 {@linkplain #toSet(Object...)}
      */
+    @Deprecated
     @SafeVarargs
-    @SuppressWarnings("unchecked")
     public static <T, C extends T> Set<T> toSetIfNullToEmpty(C... array) {
-        if (array == null) {
-            return new HashSet<>();
-        } else {
-            Set<C> list = new HashSet<>(Arrays.asList(array));
-            // JDK1.7必须强转, JDK1.8不需要
-            return (Set<T>) list;
-        }
+        return toSet(array);
     }
 
     /**
      * 将key转换为map<br>
      * Map&lt;String, ?&gt; maps = ConvertTools.toKeyMaps("a,b,c");
-     * 
+     *
      * @param keys KEY
      * @return Map
      */
     public static Map<String, ?> toKeyMaps(String... keys) {
         Map<String, ?> map = new HashMap<>();
-        for (String item : keys) {
-            String[] array = StringTools.split(item, ',');
-            for (String key : array) {
-                map.put(key, null);
+        if (keys != null) {
+            for (String item : keys) {
+                String[] array = StringTools.split(item, ',');
+                for (String key : array) {
+                    map.put(key, null);
+                }
             }
         }
         return map;
@@ -281,16 +295,18 @@ public class ConvertTools {
     /**
      * 将key的字符转换为map<br>
      * Map&lt;Character, ?&gt; maps = ConvertTools.toKeyMaps("abc");
-     * 
+     *
      * @param strings KEY
      * @return Map
      */
     public static Map<Character, ?> toCharMaps(String... strings) {
         Map<Character, ?> map = new HashMap<>();
-        for (String string : strings) {
-            char[] chars = string.toCharArray();
-            for (int i = 0, z = chars.length; i < z; i++) {
-                map.put(chars[i], null);
+        if (strings != null) {
+            for (String string : strings) {
+                char[] chars = string.toCharArray();
+                for (int i = 0, z = chars.length; i < z; i++) {
+                    map.put(chars[i], null);
+                }
             }
         }
         return map;
@@ -300,12 +316,12 @@ public class ConvertTools {
      * 数组转Map: key=数组项, value=null<br>
      * List&lt;String&gt; list = Arrays.asList("a", "b", ...);<br>
      * Map&lt;String, ?&gt; maps = ConvertTools.toMap(list);
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
      * @param <V> 值类型
-     * @return 转换后的Map, 如果array=null则返回null
+     * @return 转换后的Map, 如果array=null将返回空Map
      */
     public static <T, C extends T, V> Map<T, V> toMap(Collection<C> array) {
         return toMap(array, null);
@@ -315,18 +331,18 @@ public class ConvertTools {
      * 数组转Map: key=数组项, 所有key都会指向同一个value<br>
      * List&lt;String&gt; list = Arrays.asList("a", "b", ...);<br>
      * Map&lt;String, Boolean&gt; maps = ConvertTools.toMap(list, true);
-     * 
+     *
      * @param array 数组
      * @param value 值
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
      * @param <V> 值类型
-     * @return 转换后的Map, 如果array=null则返回null
+     * @return 转换后的Map, 如果array=null将返回空Map
      */
     @SuppressWarnings("unchecked")
     public static <T, C extends T, V> Map<T, V> toMap(Collection<C> array, V value) {
         if (array == null) {
-            return null;
+            return new HashMap<>();
         } else {
             Map<C, V> map = new HashMap<>();
             for (C field : array) {
@@ -341,41 +357,35 @@ public class ConvertTools {
      * 数组转Map: key=数组项, value=null<br>
      * List&lt;String&gt; list = Arrays.asList("a", "b", ...);<br>
      * Map&lt;String, ?&gt; maps = ConvertTools.toMapIfNullToEmpty(list);
-     * 
+     *
      * @param array 数组
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
      * @param <V> 值类型
-     * @return 转换后的Map, 如果array=null则返回EmptyHashMap
+     * @return 转换后的Map, 如果array=null则返回空Map
+     * @deprecated 改为 {@linkplain #toMap(Collection)}
      */
+    @Deprecated
     public static <T, C extends T, V> Map<T, V> toMapIfNullToEmpty(Collection<C> array) {
-        return toMapIfNullToEmpty(array, null);
+        return toMap(array, null);
     }
 
     /**
      * 数组转Map: key=数组项, 所有key都会指向同一个value<br>
      * List&lt;String&gt; list = Arrays.asList("a", "b", ...);<br>
      * Map&lt;String, Boolean&gt; maps = ConvertTools.toMapIfNullToEmpty(list, true);
-     * 
+     *
      * @param array 数组
      * @param value 值
      * @param <T> 目标类型
      * @param <C> 数组内容的类型
      * @param <V> 值类型
-     * @return 转换后的Map, 如果array=null则返回EmptyHashMap
+     * @return 转换后的Map, 如果array=null则返回空Map
+     * @deprecated 改为 {@linkplain #toMap(Collection, Object)}
      */
-    @SuppressWarnings("unchecked")
+    @Deprecated
     public static <T, C extends T, V> Map<T, V> toMapIfNullToEmpty(Collection<C> array, V value) {
-        if (array == null) {
-            return new HashMap<>();
-        } else {
-            Map<C, V> map = new HashMap<>();
-            for (C field : array) {
-                map.put(field, value);
-            }
-            // JDK1.7必须强转, JDK1.8不需要
-            return (Map<T, V>) map;
-        }
+        return toMap(array, value);
     }
 
     /**
@@ -458,6 +468,116 @@ public class ConvertTools {
         }
     }
 
+    /**
+     * 转换为数字, 尽可能按照期望类型输出
+     *
+     * @param number 源数字
+     * @param expect 期望类型
+     * @param maxScale 最大的小数位
+     * @return 数字
+     */
+    public static Number toNumber(BigDecimal number, Class<? extends Number> expect, int maxScale) {
+        if (number == null) {
+            return null;
+        }
+        BigDecimal value = clearTrailingZeros(number);
+        if (value.scale() > maxScale) {
+            value = value.setScale(maxScale, BigDecimal.ROUND_HALF_UP);
+            value = clearTrailingZeros(value);
+        }
+        if (expect == BigDecimal.class) {
+            return value;
+        }
+        if (value.scale() == 0) {
+            if (expect == BigInteger.class) {
+                return value.toBigInteger();
+            }
+            if (expect == Integer.class || expect == int.class) {
+                if (isWithinRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE)) {
+                    // 在Integer数值范围内, 返回intValue
+                    return value.intValue();
+                }
+            } else if (expect == Long.class || expect == long.class) {
+                if (isWithinRange(value, Long.MIN_VALUE, Long.MAX_VALUE)) {
+                    // 在Long数值范围内, 返回longValue
+                    return value.longValue();
+                }
+            } else if (expect == Double.class || expect == double.class) {
+                if (isWithinRange(value, Double.MIN_VALUE, Double.MAX_VALUE)) {
+                    // 在Double数值范围内, 返回doubleValue
+                    return value.doubleValue();
+                }
+            } else if (expect == Float.class || expect == float.class) {
+                if (isWithinRange(value, Float.MIN_VALUE, Float.MAX_VALUE)) {
+                    // 在Float数值范围内, 返回floatValue
+                    return value.floatValue();
+                }
+            } else if (expect == Byte.class || expect == byte.class) {
+                if (isWithinRange(value, Byte.MIN_VALUE, Byte.MAX_VALUE)) {
+                    // 在Byte数值范围内, 返回byteValue
+                    return value.byteValue();
+                }
+            } else if (expect == Short.class || expect == short.class) {
+                if (isWithinRange(value, Short.MIN_VALUE, Short.MAX_VALUE)) {
+                    // 在Short数值范围内, 返回byteValue
+                    return value.shortValue();
+                }
+            }
+            if (isWithinRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE)) {
+                // 在Integer数值范围内, 返回intValue
+                return value.intValue();
+            } else if (isWithinRange(value, Long.MIN_VALUE, Long.MAX_VALUE)) {
+                // 在Long数值范围内, 返回longValue
+                return value.longValue();
+            } else {
+                // 超过Long的范围, 返回BigDecimal
+                return value;
+            }
+        } else {
+            if (expect == Double.class || expect == double.class) {
+                if (isWithinRange(value, Double.MIN_VALUE, Double.MAX_VALUE)) {
+                    // 在Double数值范围内, 返回doubleValue
+                    return value.doubleValue();
+                }
+            } else if (expect == Float.class || expect == float.class) {
+                if (isWithinRange(value, Float.MIN_VALUE, Float.MAX_VALUE)) {
+                    // 在Float数值范围内, 返回floatValue
+                    return value.floatValue();
+                }
+            }
+            if (isWithinRange(value, Float.MIN_VALUE, Float.MAX_VALUE)) {
+                // 在Float数值范围内, 返回floatValue
+                return value.floatValue();
+            } else if (isWithinRange(value, Double.MIN_VALUE, Double.MAX_VALUE)) {
+                // 在Double数值范围内, 返回doubleValue
+                return value.doubleValue();
+            } else {
+                // 超过Double的范围, 返回BigDecimal
+                return value;
+            }
+        }
+    }
+
+    private static boolean isWithinRange(BigDecimal number, double min, double max) {
+        return number.compareTo(BigDecimal.valueOf(min)) >= 0 && number.compareTo(BigDecimal.valueOf(max)) <= 0;
+    }
+
+    /**
+     * 清除科学计数法
+     *
+     * @param number 目标数字
+     * @return 最终结果
+     */
+    public static BigDecimal clearTrailingZeros(BigDecimal number) {
+        number = number.stripTrailingZeros();
+        // new BigDecimal(20000).stripTrailingZeros(); = 2E+4 (科学计数法)
+        // 此时precision=1,scale=-4; 需要将scale设置为0
+        if (number.scale() < 0) {
+            number = number.setScale(0, BigDecimal.ROUND_HALF_UP);
+        }
+        return number;
+    }
+
     /**
      * 转换为数字
      *
@@ -470,8 +590,12 @@ public class ConvertTools {
             throw new NumberFormatException("null");
         }
         Long number = toLong(value, null);
-        if (number == null || number > Integer.MAX_VALUE) {
+        if (number == null) {
             throw new NumberFormatException(value);
+        } else if (number > Integer.MAX_VALUE) {
+            throw new NumberFormatException(value + " greater than Integer.MAX_VALUE");
+        } else if (number < Integer.MIN_VALUE) {
+            throw new NumberFormatException(value + " less than Integer.MIN_VALUE");
         } else {
             return number.intValue();
         }
@@ -486,7 +610,11 @@ public class ConvertTools {
      */
     public static Integer toInteger(String value, Integer defaults) {
         Long number = toLong(value, defaults == null ? null : Long.valueOf(defaults));
-        return number == null || number > Integer.MAX_VALUE ? null : number.intValue();
+        if (number == null || number < Integer.MIN_VALUE || number > Integer.MAX_VALUE) {
+            return defaults;
+        } else {
+            return number.intValue();
+        }
     }
 
     /**
@@ -530,21 +658,24 @@ public class ConvertTools {
         BigInteger number = null;
         if (flagIndex < 0) {
             try {
-                number = new BigInteger(value);
+                number = parseBigInteger(value);
             } catch (NumberFormatException e) {
                 return defaults;
             }
         } else {
             String[] values = StringTools.split(value, '*');
             for (String string : values) {
+                if (string.isEmpty()) {
+                    return defaults; // 格式错误
+                }
                 try {
                     if (number == null) {
-                        number = new BigInteger(string);
+                        number = parseBigInteger(string);
                     } else {
-                        number = number.multiply(new BigInteger(string));
+                        number = number.multiply(parseBigInteger(string));
                     }
                 } catch (NumberFormatException e) {
-                    return defaults;
+                    return defaults; // 格式错误
                 }
             }
         }
@@ -557,6 +688,39 @@ public class ConvertTools {
         }
     }
 
+    private static BigInteger parseBigInteger(String s) {
+        if (StringTools.isNumber(s)) {
+            return new BigInteger(s);
+        } else if (StringTools.countCharacter(s, ':') == 2) {
+            try {
+                // 时间, 带毫秒或不带毫秒, 10:20:30, 10:5:8.360
+                return BigInteger.valueOf(DateTools.parseTime(s));
+            } catch (Exception e) {
+                throw new NumberFormatException(s);
+            }
+        } else if (s.startsWith("P") || s.startsWith("p") || s.startsWith("-P") || s.startsWith("-p")) {
+            try {
+                // Duration since jdk8
+                Class.forName("java.time.Duration");
+            } catch (ClassNotFoundException e) {
+                throw new NumberFormatException(s);
+            }
+            try {
+                // P2DT3H4M / PT15H / PT15M
+                return BigInteger.valueOf(java.time.Duration.parse(s).toMillis());
+            } catch (Exception e) {
+                throw new NumberFormatException(s);
+            }
+        } else {
+            Long number = tryParseByteString(s);
+            if (number == null) {
+                throw new NumberFormatException(s);
+            } else {
+                return BigInteger.valueOf(number);
+            }
+        }
+    }
+
     /**
      * 转换为数字
      *
@@ -569,8 +733,12 @@ public class ConvertTools {
             throw new NumberFormatException("null");
         }
         Double number = toDouble(value, null);
-        if (number == null || number > Float.MAX_VALUE) {
+        if (number == null) {
             throw new NumberFormatException(value);
+        } else if (number > Float.MAX_VALUE) {
+            throw new NumberFormatException(value + " greater than Float.MAX_VALUE");
+        } else if (number < Float.MIN_VALUE) {
+            throw new NumberFormatException(value + " less than Float.MIN_VALUE");
         } else {
             return number.floatValue();
         }
@@ -585,7 +753,11 @@ public class ConvertTools {
      */
     public static Float toFloat(String value, Float defaults) {
         Double number = toDouble(value, defaults == null ? null : Double.valueOf(defaults));
-        return number == null || number > Float.MAX_VALUE ? null : number.floatValue();
+        if (number == null || number > Float.MAX_VALUE) {
+            return defaults;
+        } else {
+            return number.floatValue();
+        }
     }
 
     /**
@@ -656,9 +828,28 @@ public class ConvertTools {
         }
     }
 
+    /** 数字转换为BigDecimal **/
+    public static BigDecimal toBigDecimal(Number number) {
+        if (number == null) {
+            return null;
+        } else if (number instanceof BigDecimal) {
+            return (BigDecimal) number;
+        } else {
+            return new BigDecimal(String.valueOf(number));
+        }
+    }
+
+    /** BigDecimal转换为字符串 (清除小数点后末尾的0,不使用科学计数法) **/
+    public static String toPlainString(BigDecimal number) {
+        if (number == null) {
+            return null;
+        }
+        return clearTrailingZeros(number).toPlainString();
+    }
+
     /**
      * 转换为Boolean
-     * 
+     *
      * @param value 源字符串
      * @return 解析结果
      * @throws IllegalArgumentException 格式错误
@@ -667,17 +858,18 @@ public class ConvertTools {
         if (VerifyTools.isBlank(value)) {
             throw new IllegalArgumentException("null");
         }
-        Boolean number = toBoolean(value, null);
-        if (number == null) {
+        value = value.trim();
+        Boolean b = StringTools.isPositive(value, null);
+        if (b == null) {
             throw new IllegalArgumentException(value);
         } else {
-            return number;
+            return b;
         }
     }
 
     /**
      * 转换为Boolean
-     * 
+     *
      * @param value 源字符串
      * @param defaults 默认值, 在表达式为空/表达式格式错误/表达式结果不是数字时返回默认值
      * @return 解析结果
@@ -686,16 +878,8 @@ public class ConvertTools {
         if (VerifyTools.isBlank(value)) {
             return defaults;
         }
-
         value = value.trim();
-
-        if (Boolean.TRUE.equals(StringTools.isPositive(value, null))) {
-            return true;
-        } else if (Boolean.FALSE.equals(StringTools.isNegative(value, null))) {
-            return false;
-        } else {
-            return defaults;
-        }
+        return StringTools.isPositive(value, defaults);
     }
 
     /**
@@ -808,7 +992,8 @@ public class ConvertTools {
     private static final long tebibyte = gibibyte * kibibyte;
     private static final long pebibyte = tebibyte * kibibyte;
     private static final long exbibyte = pebibyte * kibibyte;
-    private static final Map<String, Long> BYTE_UNITS = new HashMap<>();
+    private static final Map<String, Long> BYTE_UNITS = new LinkedHashMap<>();
+
     static { // 单位对应的倍数
         BYTE_UNITS.put("B", 1L);
         BYTE_UNITS.put("KB", kibibyte);
@@ -858,22 +1043,31 @@ public class ConvertTools {
      */
     public static long parseByteString(String string) {
         VerifyTools.requireNotBlank(string, "ByteString");
+        Long number = tryParseByteString(string);
+        if (number == null) {
+            throw new NumberFormatException(string);
+        } else {
+            return number;
+        }
+    }
+
+    private static Long tryParseByteString(String string) {
         // 截取数字和单位
         String number = string.trim();
         String unit = null;
         char[] chars = number.toCharArray();
         for (int i = 0; i < chars.length; i++) {
-            int p = chars.length - 1 - i;
-            if (Character.isDigit(chars[p])) {
+            int p = chars.length - i;
+            if (Character.isDigit(chars[p - 1])) {
                 if (i > 0) {
                     number = string.substring(0, p);
-                    unit = string.substring(p + 1);
+                    unit = string.substring(p);
                 }
                 break;
             }
         }
         if (VerifyTools.isBlank(number)) {
-            throw new NumberFormatException(string);
+            return null;
         }
         // 计算数值
         if (unit == null) {
@@ -881,9 +1075,10 @@ public class ConvertTools {
         } else {
             Long rate = BYTE_UNITS.get(unit.toUpperCase());
             if (rate == null) {
-                throw new NumberFormatException(string);
+                return null;
+            } else {
+                return Long.parseLong(number) * rate;
             }
-            return Long.parseLong(number) * rate;
         }
     }
 
@@ -901,23 +1096,23 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param list 数组
-     * @param seprator 分隔符, 可为空
+     * @param separator 分隔符, 可为空
      * @return 合并后的字符串
      */
-    public static String joinToString(Collection<?> list, String seprator) {
-        return joinToString(list, seprator, false);
+    public static String joinToString(Collection<?> list, String separator) {
+        return joinToString(list, separator, false);
     }
 
     /**
      * 将数组合并为字符串
      *
      * @param list 数组
-     * @param seprator 分隔符
+     * @param separator 分隔符
      * @return 合并后的字符串
      * @since 4.1.0
      */
-    public static String joinToString(Collection<?> list, char seprator) {
-        return joinToString(list, seprator, false);
+    public static String joinToString(Collection<?> list, char separator) {
+        return joinToString(list, separator, false);
     }
 
     /**
@@ -934,23 +1129,23 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param array 数组
-     * @param seprator 分隔符, 可为空
+     * @param separator 分隔符, 可为空
      * @return 合并后的字符串
      */
-    public static String joinToString(Object[] array, String seprator) {
-        return joinToString(array, seprator, false);
+    public static String joinToString(Object[] array, String separator) {
+        return joinToString(array, separator, false);
     }
 
     /**
      * 将数组合并为字符串
      *
      * @param array 数组
-     * @param seprator 分隔符
+     * @param separator 分隔符
      * @return 合并后的字符串
      * @since 4.1.0
      */
-    public static String joinToString(Object[] array, char seprator) {
-        return joinToString(array, seprator, false);
+    public static String joinToString(Object[] array, char separator) {
+        return joinToString(array, separator, false);
     }
 
     /**
@@ -968,19 +1163,19 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param list 数组
-     * @param seprator 分隔符, 可为空
+     * @param separator 分隔符, 可为空
      * @param wrap 是否用括号包起来
      * @return 合并后的字符串
      */
-    public static String joinToString(Collection<?> list, String seprator, boolean wrap) {
+    public static String joinToString(Collection<?> list, String separator, boolean wrap) {
         if (list == null) {
             return null;
         }
         StringBuilder buffer = new StringBuilder();
         if (VerifyTools.isNotBlank(list)) {
             for (Object tmp : list) {
-                if (seprator != null && buffer.length() > 0) {
-                    buffer.append(seprator);
+                if (separator != null && buffer.length() > 0) {
+                    buffer.append(separator);
                 }
                 buffer.append(tmp);
             }
@@ -996,11 +1191,11 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param list 数组
-     * @param seprator 分隔符
+     * @param separator 分隔符
      * @param wrap 是否用括号包起来
      * @return 合并后的字符串
      */
-    public static String joinToString(Collection<?> list, char seprator, boolean wrap) {
+    public static String joinToString(Collection<?> list, char separator, boolean wrap) {
         if (list == null) {
             return null;
         }
@@ -1008,7 +1203,7 @@ public class ConvertTools {
         if (VerifyTools.isNotBlank(list)) {
             for (Object tmp : list) {
                 if (buffer.length() > 0) {
-                    buffer.append(seprator);
+                    buffer.append(separator);
                 }
                 buffer.append(tmp);
             }
@@ -1035,19 +1230,19 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param array 数组
-     * @param seprator 分隔符, 可为空
+     * @param separator 分隔符, 可为空
      * @param wrap 是否用括号包起来
      * @return 合并后的字符串
      */
-    public static String joinToString(Object[] array, String seprator, boolean wrap) {
+    public static String joinToString(Object[] array, String separator, boolean wrap) {
         if (array == null) {
             return null;
         }
         StringBuilder buffer = new StringBuilder();
         if (VerifyTools.isNotBlank(array)) {
             for (Object tmp : array) {
-                if (seprator != null && buffer.length() > 0) {
-                    buffer.append(seprator);
+                if (separator != null && buffer.length() > 0) {
+                    buffer.append(separator);
                 }
                 buffer.append(tmp);
             }
@@ -1063,12 +1258,12 @@ public class ConvertTools {
      * 将数组合并为字符串
      *
      * @param array 数组
-     * @param seprator 分隔符
+     * @param separator 分隔符
      * @param wrap 是否用括号包起来
      * @return 合并后的字符串
      * @since 4.1.0
      */
-    public static String joinToString(Object[] array, char seprator, boolean wrap) {
+    public static String joinToString(Object[] array, char separator, boolean wrap) {
         if (array == null) {
             return null;
         }
@@ -1076,7 +1271,7 @@ public class ConvertTools {
         if (VerifyTools.isNotBlank(array)) {
             for (Object tmp : array) {
                 if (buffer.length() > 0) {
-                    buffer.append(seprator);
+                    buffer.append(separator);
                 }
                 buffer.append(tmp);
             }
@@ -1088,13 +1283,107 @@ public class ConvertTools {
         return buffer.toString();
     }
 
+    /**
+     * 解析基本数据类型的字段值
+     *
+     * @param fieldName 字段名
+     * @param fieldValue 字段值
+     * @return 新值
+     */
+    public static Object tryParsePrimitiveValue(String fieldName, String fieldValue) {
+        fieldValue = StringTools.trim(fieldValue);
+        if (VerifyTools.isBlank(fieldValue)) {
+            return null;
+        }
+        if ("true".equals(fieldValue)) {
+            return true;
+        } else if ("false".equals(fieldValue)) {
+            return false;
+        }
+
+        if (StringTools.isNumber(fieldValue)) {
+            if ("0".equals(fieldValue) || fieldValue.startsWith("0.") || !fieldValue.startsWith("0")) {
+                try {
+                    return ConvertTools.toNumber(fieldValue);
+                } catch (NumberFormatException ignore) {
+                }
+            }
+        } else if (fieldName != null) {
+            try {
+                if (fieldName.endsWith("OfMinInDay") || fieldName.endsWith("OfMinDay")) {
+                    Date date = DateTools.parse(fieldValue);
+                    return DateTools.toStartTime(date);
+                } else if (fieldName.endsWith("OfMaxInDay") || fieldName.endsWith("OfMaxDay")) {
+                    Date date = DateTools.parse(fieldValue);
+                    return DateTools.toEndTime(date);
+                } else if (fieldName.endsWith("Date") || fieldName.endsWith("_DATE")) {
+                    return DateTools.parse(fieldValue);
+                } else if (fieldName.endsWith("Time") || fieldName.endsWith("_TIME")) {
+                    return DateTools.parse(fieldValue);
+                } else if (isLikeDateFormat(fieldValue)) {
+                    return DateTools.parse(fieldValue);
+                }
+            } catch (IllegalArgumentException ignore) {
+            }
+        } else if (isLikeDateFormat(fieldValue)) {
+            try {
+                return DateTools.parse(fieldValue);
+            } catch (IllegalArgumentException ignore) {
+            }
+        }
+
+        return fieldValue;
+    }
+
+    /** 支持哪些日期格式 **/
+    private static final List<String> DATE_FORMATS = Arrays.asList(
+            "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss.SSS"
+    );
+
+    private static boolean isLikeDateFormat(String source) {
+        for (String format : DATE_FORMATS) {
+            if (isDateFormatMatches(source, format)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // 检查字符串是不是日期时间格式
+    private static boolean isDateFormatMatches(String source, String format) {
+        if (VerifyTools.isAnyBlank(source, format)) {
+            return false;
+        }
+        int sLength = source.length();
+        int fLength = format.length();
+        if (sLength != fLength) {
+            return false;
+        }
+        for (int i = 0; i < fLength; i++) {
+            char s = source.charAt(i);
+            char c = format.charAt(i);
+            if (c == 'y' || c == 'M' || c == 'd' || c == 'H' || c == 'h' || c == 'm' || c == 's' || c == 'S') {
+                // 遇到yyyy MM dd HH mm ss SSS之类的字符, 则原字符必须是数字
+                if (s < '0' || s > '9') {
+                    return false;
+                }
+            } else {
+                // 遇到其他字符, 则原字符必须与该字符相等
+                if (s != c) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     /**
      * 压缩列表, 保留指定数目
      *
-     * @author zhaohuihua
      * @param list 列表
      * @param max 保留数目
      * @return 压缩后的列表
+     * @author zhaohuihua
      */
     public static <T> List<T> compressList(List<T> list, int max) {
         int total = list.size();
@@ -1119,7 +1408,7 @@ public class ConvertTools {
             // 那要取多少次才能减少掉5个点呢, 取最小公倍数
             // 就是210 = 5 * 6 * 7
             int end = diff * interval * (interval + 1);
-            for (int i = 0; i < total;) {
+            for (int i = 0; i < total; ) {
                 compressed.add(list.get(i));
                 i += interval;
                 // 达到end之前多隔一个点取一个
@@ -1133,20 +1422,61 @@ public class ConvertTools {
         return compressed;
     }
 
+    /**
+     * 生成迪卡尔积
+     *
+     * @param sources 原二维数组
+     * @return 迪卡尔积数组
+     */
+    // http://blog.sina.com.cn/s/blog_a6ae0b3b0101624x.html
+    public static <T> List<List<T>> toCartesianList(List<List<T>> sources) {
+        // 计算出笛卡尔积行数
+        int rows = sources.size() > 0 ? 1 : 0;
+        for (Collection<T> data : sources) {
+            rows *= data.size();
+        }
+
+        // 笛卡尔积索引记录
+        int[] record = new int[sources.size()];
+
+        List<List<T>> results = new ArrayList<>();
+        // 产生笛卡尔积
+        for (int i = 0; i < rows; i++) {
+            List<T> row = new ArrayList<>();
+            // 生成笛卡尔积的每组数据
+            for (int index = 0; index < record.length; index++) {
+                row.add(sources.get(index).get(record[index]));
+            }
+
+            results.add(row);
+            toCartesianList(sources, record, sources.size() - 1);
+        }
+        return results;
+    }
+
+    private static <T> void toCartesianList(List<List<T>> source, int[] record, int level) {
+        record[level] = record[level] + 1;
+
+        if (record[level] >= source.get(level).size() && level > 0) {
+            record[level] = 0;
+            toCartesianList(source, record, level - 1);
+        }
+    }
+
     /**
      * 清除Map中的空值
-     * 
+     *
      * @param map Map对象
      * @deprecated move to {@link MapTools#clearBlankValue(Map)}
      */
     @Deprecated
-    public static void clearBlankValue(Map<String, Object> map) {
+    public static void clearBlankValue(Map<?, ?> map) {
         MapTools.clearBlankValue(map);
     }
 
     /**
      * 解析请求参数
-     * 
+     *
      * @param params 请求参数
      * @return Map
      * @deprecated move to {@link MapTools#parseRequestParams(String)}
@@ -1158,7 +1488,7 @@ public class ConvertTools {
 
     /**
      * 解析请求参数
-     * 
+     *
      * @param params 请求参数
      * @param charset 字符编码: 如果不为空, 将对value执行URLDecoder.decode(value, charset)
      * @return Map
@@ -1172,7 +1502,7 @@ public class ConvertTools {
     /**
      * 获取字符串类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getString("xxx")将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @return 字符串
@@ -1186,7 +1516,7 @@ public class ConvertTools {
     /**
      * 获取指定类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getValue("xxx", Boolean.class)将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
@@ -1201,7 +1531,7 @@ public class ConvertTools {
     /**
      * 获取指定类型的Map值<br>
      * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getValue("xxx", Boolean.class)将返回null
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param defaults 值不存在或值类型不匹配时返回的默认值
@@ -1218,7 +1548,7 @@ public class ConvertTools {
      * 获取指定类型的Map值列表<br>
      * 注意: 该方法不进行类型转换, 如果key指向的对象不是Iterable, 将返回null或空列表<br>
      * 注意: 如果Iterable中存在类型不匹配的元素, 也会返回null或空列表
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
@@ -1234,7 +1564,7 @@ public class ConvertTools {
      * 获取指定类型的Map值列表<br>
      * 注意: 该方法不进行类型转换, 如果key指向的对象不是Iterable, 将返回null或空列表<br>
      * 注意: 如果Iterable中存在类型不匹配的元素, 也会返回null或空列表
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param nullToEmpty 值不存在或值类型不匹配时是否返回空列表
@@ -1250,15 +1580,15 @@ public class ConvertTools {
     /**
      * 过滤数据字段<br>
      * <pre>
-        Map&lt;String, Object&gt; list = [ { main:{}, detail:{}, subject:{}, finance:[], target:[] } ];
-        String mainFields = "main:id,projectName,publishStatus";
-        String detailFields = "detail:id,totalAmount,holdingTime";
-        String subjectFields = "subject:id,customerName,industryCategory";
-        String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
-        String targetFields = "target:id,industryCategory,latestMkt,latestPer";
-        ConvertTools.filterFields(list, mainFields, detailFields, subjectFields, financeFields, targetFields);
+     * Map&lt;String, Object&gt; list = [ { main:{}, detail:{}, subject:{}, finance:[], target:[] } ];
+     * String mainFields = "main:id,projectName,publishStatus";
+     * String detailFields = "detail:id,totalAmount,holdingTime";
+     * String subjectFields = "subject:id,customerName,industryCategory";
+     * String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
+     * String targetFields = "target:id,industryCategory,latestMkt,latestPer";
+     * ConvertTools.filterFields(list, mainFields, detailFields, subjectFields, financeFields, targetFields);
      * </pre>
-     * 
+     *
      * @param list 过滤前的数据
      * @param fields 需要保留的字段列表
      * @return 过滤后的数据
@@ -1272,15 +1602,15 @@ public class ConvertTools {
     /**
      * 过滤数据字段<br>
      * <pre>
-        Map&lt;String, Object&gt; data = { main:{}, detail:{}, subject:{}, finance:[], target:[] };
-        String mainFields = "main:id,projectName,publishStatus";
-        String detailFields = "detail:id,totalAmount,holdingTime";
-        String subjectFields = "subject:id,customerName,industryCategory";
-        String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
-        String targetFields = "target:id,industryCategory,latestMkt,latestPer";
-        ConvertTools.filterFields(data, mainFields, detailFields, subjectFields, financeFields, targetFields);
+     * Map&lt;String, Object&gt; data = { main:{}, detail:{}, subject:{}, finance:[], target:[] };
+     * String mainFields = "main:id,projectName,publishStatus";
+     * String detailFields = "detail:id,totalAmount,holdingTime";
+     * String subjectFields = "subject:id,customerName,industryCategory";
+     * String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
+     * String targetFields = "target:id,industryCategory,latestMkt,latestPer";
+     * ConvertTools.filterFields(data, mainFields, detailFields, subjectFields, financeFields, targetFields);
      * </pre>
-     * 
+     *
      * @param data 过滤前的数据
      * @param fields 需要保留的字段列表
      * @return 过滤后的数据
@@ -1291,4 +1621,51 @@ public class ConvertTools {
         return MapTools.filterFields(data, fields);
     }
 
+    /** LocalDateTime转换为Date **/
+    public static Date toDate(LocalDateTime source) {
+        ZoneId zoneId = ZoneId.systemDefault();
+        // Combines this date-time with a time-zone to create a ZonedDateTime.
+        ZonedDateTime zdt = source.atZone(zoneId);
+        return Date.from(zdt.toInstant());
+    }
+
+    /** LocalDate转换为Date **/
+    public static Date toDate(LocalDate source) {
+        ZoneId zoneId = ZoneId.systemDefault();
+        // Combines this date-time with a time-zone to create a ZonedDateTime.
+        ZonedDateTime zdt = source.atStartOfDay().atZone(zoneId);
+        return Date.from(zdt.toInstant());
+    }
+
+    /** LocalTime转换为Date **/
+    public static Date toDate(LocalTime source) {
+        LocalDateTime localDateTime = LocalDateTime.of(LocalDate.now(), source);
+        ZoneId zoneId = ZoneId.systemDefault();
+        // Combines this date-time with a time-zone to create a ZonedDateTime.
+        ZonedDateTime zdt = localDateTime.atZone(zoneId);
+        return Date.from(zdt.toInstant());
+    }
+
+    /** Date转换为LocalDateTime **/
+    public static LocalDateTime toLocalDateTime(Date source) {
+        Instant instant = source.toInstant();
+        ZoneId zone = ZoneId.systemDefault();
+        return LocalDateTime.ofInstant(instant, zone);
+    }
+
+    /** Date转换为LocalDate **/
+    public static LocalDate toLocalDate(Date source) {
+        Instant instant = source.toInstant();
+        ZoneId zone = ZoneId.systemDefault();
+        LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
+        return localDateTime.toLocalDate();
+    }
+
+    /** Date转换为LocalTime **/
+    public static LocalTime toLocalTime(Date source) {
+        Instant instant = source.toInstant();
+        ZoneId zone = ZoneId.systemDefault();
+        LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
+        return localDateTime.toLocalTime();
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
index 480641bf418304086088606a161d30c861bf3cf5..8b4a3e50a0db4ac5dacbc0c293c4ca9e025d13d6 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/DateTools.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.tools.utils;
 
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.time.Month;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
@@ -125,16 +126,16 @@ public class DateTools {
                 this.length = 0;
                 // X=+/-hh, XX=+/-hhmm, 3个X=+/-hh:mm
                 if (regexp.endsWith("XXX")) {
-                    regexp = StringTools.removeRight(regexp, 'X') + "[+-]\\d\\d:\\d\\d";
+                    regexp = StringTools.trimRight(regexp, 'X') + "[+-]\\d\\d:\\d\\d";
                 } else if (regexp.endsWith("XX")) {
-                    regexp = StringTools.removeRight(regexp, 'X') + "[+-]\\d{4}";
+                    regexp = StringTools.trimRight(regexp, 'X') + "[+-]\\d{4}";
                 } else if (regexp.endsWith("X")) {
-                    regexp = StringTools.removeRight(regexp, 'X') + "[+-]\\d{2}";
+                    regexp = StringTools.trimRight(regexp, 'X') + "[+-]\\d{2}";
                 }
             } else if (regexp.endsWith("Z")) { // GMT时区字符
                 this.length = 0;
                 // GMT或GMT+/-hh:mm或+/-hhmm
-                regexp = StringTools.removeRight(regexp, 'Z') + "(?:\\w+|\\w+[+-]\\d\\d:\\d\\d|[+-]\\d{4})";
+                regexp = StringTools.trimRight(regexp, 'Z') + "(?:\\w+|\\w+[+-]\\d\\d:\\d\\d|[+-]\\d{4})";
             }
             { // 常量字符
                 Matcher matcher = CONST_CHAR.matcher(regexp);
@@ -199,11 +200,14 @@ public class DateTools {
      * parse("2000-10-20");<br>
      * parse("2018/8/20 15:25:35");<br>
      * parse("8/20/2018 15:25:35");<br>
+     * parse("2000.10.20 15:25:35.450");<br>
+     * parse("2000.10.20");<br>
+     * parse("15:25:35.450"); // 时间<br>
+     * parse("15:25:35"); // 时间<br>
      * parse("20001020152535450");<br>
      * parse("20001020152535");<br>
-     * parse("20001020");<br>
-     * parse("15:25:35");<br>
-     * parse("152535");<br>
+     * parse("20001020"); // 日期<br>
+     * parse("152535"); // 时间<br>
      * parse("Fri Oct 20 15:25:35 CST 2000");<br>
      * 解析年月日格式说明<br>
      * 年月日(中式): 2018/8/8, 2018/8/20, 2018/12/12<br>
@@ -220,18 +224,22 @@ public class DateTools {
         if (string == null || string.trim().length() == 0) {
             return null;
         }
-        if (ZONED.supported(string)) {
-            // 解析带时区的时间, 如: Fri Oct 20 15:25:35 CST 2000
-            return ZONED.parse(string);
-        }
         String datetime = string.trim();
+        // 空格位置
         int spaceIndex = -1;
+        // 横杠位置
         int hbarIndex = -1;
+        // 斜杠位置
         int slashIndex = -1;
+        // 冒号位置
         int colonIndex = -1;
+        // 点的位置
         int dotIndex = -1;
+        // 其他字符位置 (0-9以及-/:.之外的其他字符)
         int otherCharIndex = -1;
+        // 是否存在中文日期单位
         boolean existChineseDateUnits = false;
+        // 是否存在中文时间单位
         boolean existChineseTimeUnits = false;
         char[] chars = datetime.toCharArray();
         for (char i = 0; i < chars.length; i++) {
@@ -268,6 +276,11 @@ public class DateTools {
             }
         }
 
+        if (otherCharIndex >= 0 && ZONED.supported(string)) {
+            // 解析带时区的时间, 如: Fri Oct 20 15:25:35 CST 2000
+            return ZONED.parse(string);
+        }
+
         if (existChineseDateUnits || existChineseTimeUnits) {
             // 2018年7月8日10时20分30秒400毫秒
             Calendar calendar = Calendar.getInstance();
@@ -326,47 +339,69 @@ public class DateTools {
         }
 
         if (otherCharIndex >= 0) {
+            // 非标准格式: 存在0-9以及-/:.之外的其他字符, 使用PARSERS解析
             return PARSERS.parse(datetime);
         } else if (spaceIndex > 0) {
+            // 有空格, 尝试按日期+时间解析
             String[] array = StringTools.split(datetime, ' ');
             if (array.length != 2) {
+                // 非标准格式: 不止一个空格
                 return PARSERS.parse(datetime);
             }
-            if (colonIndex >= 0 && colonIndex < spaceIndex || dotIndex >= 0 && dotIndex < spaceIndex) {
-                // 日期部分有冒号或点
+            // 日期部分有没有冒号
+            boolean colonInDate = colonIndex >= 0 && colonIndex < spaceIndex;
+            if (colonInDate) {
+                // 非标准格式: 日期部分有冒号
                 return PARSERS.parse(datetime);
-            } else if (hbarIndex >= 0 && hbarIndex > spaceIndex || slashIndex >= 0 && slashIndex > spaceIndex) {
-                // 时间部分有横框或斜杠
+            }
+            // 时间部分有没有冒号
+            boolean colonInTime = colonIndex >= 0 && colonIndex > spaceIndex;
+            // 时间部分有没有横杠
+            boolean hbarInTime = hbarIndex >= 0 && hbarIndex > spaceIndex;
+            // 时间部分有没有斜杠
+            boolean slashInTime = slashIndex >= 0 && slashIndex > spaceIndex;
+            if (hbarInTime || slashInTime || !colonInTime) {
+                // 非标准格式: 时间部分有横框或斜杠, 或时间部分没有冒号
                 return PARSERS.parse(datetime);
             }
+            // 日期部分有没有横杠
+            boolean hbarInDate = hbarIndex > 0 && hbarIndex < spaceIndex;
+            // 日期部分有没有斜杠
+            boolean slashInDate = slashIndex > 0 && slashIndex < spaceIndex;
+            // 日期部分有没有点
+            boolean dotInDate = dotIndex >= 0 && dotIndex < spaceIndex;
             Date date;
-            if ((slashIndex < 0 || slashIndex > spaceIndex) && (hbarIndex > 0 && hbarIndex < spaceIndex)) {
-                // 横杠分隔的日期: 没有斜杠,有横杠且横杠的位置在日期部分
+            if (hbarInDate && !slashInDate && !dotInDate) {
+                // 横杠分隔的日期: 日期部分有横杠, 没有斜杠, 没有点
                 date = parseYyyyMMdd(array[0], '-');
-            } else if ((hbarIndex < 0 || hbarIndex > spaceIndex) && (slashIndex > 0 && slashIndex < spaceIndex)) {
-                // 斜杠分隔的日期: 没有横杠,有斜杠且斜杠的位置在日期部分
+            } else if (slashInDate && !hbarInDate && !dotInDate) {
+                // 斜杠分隔的日期: 日期部分有斜杠, 没有横杠, 没有点
                 date = parseYyyyMMdd(array[0], '/');
+            } else if (dotInDate && !slashInDate && !hbarInDate) {
+                // 点分隔的日期: 日期部分有点, 没有斜杠, 没有横杠
+                date = parseYyyyMMdd(array[0], '.');
             } else {
                 return PARSERS.parse(datetime);
             }
-            if (colonIndex > spaceIndex) {
-                // 时间部分有冒号
-                return parseTime(date, array[1]);
-            } else {
-                return PARSERS.parse(datetime);
-            }
-        } else if (hbarIndex < 0 && slashIndex < 0 && colonIndex > 0) {
-            // 时间: 没有横杠和斜杠,有冒号
-            return parseTime(toStartTime(new Date()), datetime);
-        } else if (colonIndex < 0 && dotIndex < 0 && slashIndex < 0 && hbarIndex > 0) {
-            // 横杠分隔的日期: 没有冒号和点,没有斜杠,有横杠
-            return parseYyyyMMdd(datetime, '-');
-        } else if (colonIndex < 0 && dotIndex < 0 && hbarIndex < 0 && slashIndex > 0) {
-            // 横杠分隔的日期: 没有冒号和点,没有横杠,有斜杠
-            return parseYyyyMMdd(datetime, '/');
+            // 解析时间部分
+            return parseTime(date, array[1]);
         } else {
-            return PARSERS.parse(datetime);
+            // 没有空格, 判断是日期还是时间
+            if (colonIndex > 0 && hbarIndex < 0 && slashIndex < 0) {
+                // 时间: 有冒号,没有横杠和斜杠
+                return parseTime(toStartTime(new Date()), datetime);
+            } else if (hbarIndex > 0 && colonIndex < 0 && dotIndex < 0 && slashIndex < 0) {
+                // 横杠分隔的日期: 有横杠,没有冒号,没有点,没有斜杠
+                return parseYyyyMMdd(datetime, '-');
+            } else if (slashIndex > 0 && colonIndex < 0 && dotIndex < 0 && hbarIndex < 0) {
+                // 横杠分隔的日期: 有斜杠,没有冒号,没有点,没有横杠
+                return parseYyyyMMdd(datetime, '/');
+            } else if (dotIndex > 0 && colonIndex < 0 && hbarIndex < 0 && slashIndex < 0) {
+                // 点分隔的日期: 有点,没有冒号,没有横杠,没有斜杠
+                return parseYyyyMMdd(datetime, '.');
+            }
         }
+        return PARSERS.parse(datetime);
     }
 
     /**
@@ -389,8 +424,8 @@ public class DateTools {
             throw new IllegalArgumentException(desc);
         }
         int longCount = 0;
-        for (int i = 0; i < array.length; i++) {
-            if (array[i].length() > 2) {
+        for (String s : array) {
+            if (s.length() > 2) {
                 longCount++;
             }
         }
@@ -450,7 +485,7 @@ public class DateTools {
      * @param string 时间字符串
      * @return 时间毫秒数
      */
-    private static long parseTime(String string) {
+    static long parseTime(String string) {
         if (string == null || string.trim().length() == 0) {
             return 0;
         }
@@ -485,8 +520,8 @@ public class DateTools {
         if (array.length != 3) { // 不是三个数字
             throw new IllegalArgumentException(desc);
         }
-        for (int i = 0; i < array.length; i++) {
-            if (array[i].length() > 2) {
+        for (String s : array) {
+            if (s.length() > 2) {
                 throw new IllegalArgumentException(desc);
             }
         }
@@ -1021,7 +1056,7 @@ public class DateTools {
         return calendar.getTime();
     }
 
-    /** 日期设置年 **/
+    /** 日期设置年份 **/
     public static Date setYear(Date date, int year) {
         Calendar calendar = Calendar.getInstance();
         calendar.setTime(date);
@@ -1029,7 +1064,7 @@ public class DateTools {
         return calendar.getTime();
     }
 
-    /** 日期设置月(从0开始) **/
+    /** 日期设置月份 (从0开始; 即传入0表示1月,1表示2月) **/
     public static Date setMonth(Date date, int month) {
         Calendar calendar = Calendar.getInstance();
         calendar.setTime(date);
@@ -1037,13 +1072,13 @@ public class DateTools {
         return calendar.getTime();
     }
 
-    // /** 日期设置月 **/
-    // public static Date setMonth(Date date, Month month) {
-    //     Calendar calendar = Calendar.getInstance();
-    //     calendar.setTime(date);
-    //     calendar.set(Calendar.MONTH, month.ordinal());
-    //     return calendar.getTime();
-    // }
+     /** 日期设置月份 **/
+     public static Date setMonth(Date date, Month month) {
+         Calendar calendar = Calendar.getInstance();
+         calendar.setTime(date);
+         calendar.set(Calendar.MONTH, month.ordinal());
+         return calendar.getTime();
+     }
 
     /** 日期设置日 **/
     public static Date setDay(Date date, int day) {
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/DeepEqualsAssertion.java b/able/src/main/java/com/gitee/qdbp/tools/utils/DeepEqualsAssertion.java
index c1fa81772761f18352f33bda1780cee3da00cda2..8ffe59b370292419c6b42d4e6f17dd2c181c60e9 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/DeepEqualsAssertion.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/DeepEqualsAssertion.java
@@ -340,8 +340,10 @@ class DeepEqualsAssertion {
     private static boolean equals(Object actual, Object expected) {
         if (actual == null && expected == null) {
             return true;
-        } else if (actual == null ^ expected == null) {
+        } else if (actual == null || expected == null) {
             return false;
+        } else if (actual instanceof String && expected instanceof String) {
+            return AssertTools.checkTextEquals((String) actual, (String) expected);
         } else {
             return expected.equals(actual) && actual.equals(expected);
         }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
new file mode 100644
index 0000000000000000000000000000000000000000..294ab958348b727eb41e0a7a9ff7e23fb1ec2071
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/FormatTools.java
@@ -0,0 +1,197 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import com.gitee.qdbp.json.JsonFeature;
+import com.gitee.qdbp.json.JsonFeature.Serialization;
+
+/**
+ * 格式化工具
+ *
+ * @author zhaohuihua
+ * @version 20230330
+ * @since 5.5.13
+ */
+public class FormatTools {
+
+    /** 静态工具类私有构造方法 **/
+    private FormatTools() {
+    }
+
+    /**
+     * 格式化字符串, 支持json和xml的处理
+     * @param string 源字符串
+     * @return 格式化后的字符串
+     * @since 5.5.14
+     */
+    public static String formatString(String string) {
+        if (VerifyTools.isXmlString(string)) {
+            return FormatTools.formatXml(string);
+        } else if (VerifyTools.isJsonObjectString(string)) {
+            Object object = JsonTools.parseAsMap(string);
+            return formatJson(object);
+        } else if (VerifyTools.isJsonArrayString(string)) {
+            Object object = JsonTools.parseAsMaps(string);
+            return formatJson(object);
+        } else {
+            return string;
+        }
+    }
+
+    /**
+     * 格式化Json字符串
+     *
+     * @param json 源字符串
+     * @return 格式化后的字符串
+     * @since 5.5.13
+     */
+    public static String formatJson(String json) {
+        if (VerifyTools.isBlank(json)) {
+            return json;
+        }
+        try {
+            if (VerifyTools.isJsonObjectString(json)) {
+                Object object = JsonTools.parseAsMap(json);
+                return formatJson(object);
+            } else if (VerifyTools.isJsonArrayString(json)) {
+                Object object = JsonTools.parseAsMaps(json);
+                return formatJson(object);
+            } else {
+                return json;
+            }
+        } catch (Exception ignore) {
+            return json;
+        }
+    }
+
+    /**
+     * 格式化Json字符串
+     *
+     * @param object 源对象
+     * @return 格式化后的字符串
+     * @since 5.5.13
+     */
+    public static String formatJson(Object object) {
+        Serialization serialization = JsonFeature.serialization()
+                .setQuoteFieldNames(false)
+                .setPrettyFormat(true)
+                .lockToUnmodifiable();
+        return JsonTools.toJsonString(object, serialization);
+    }
+
+    /**
+     * 格式化XML字符串
+     *
+     * @param xml 源字符串
+     * @return 格式化后的字符串
+     * @since 5.5.13
+     */
+    public static String formatXml(String xml) {
+        if (VerifyTools.isBlank(xml) || !VerifyTools.isXmlString(xml)) {
+            return xml;
+        }
+        Reader reader = new StringReader(xml);
+        try (StringWriter writer = new StringWriter()) {
+            formatXml(reader, writer);
+            return writer.getBuffer().toString();
+        } catch (IOException e) {
+            return xml;
+        }
+    }
+
+    /**
+     * 格式化XML字符串
+     *
+     * @param reader 输入
+     * @param writer 输出
+     * @since 5.5.13
+     */
+    public static void formatXml(Reader reader, Writer writer) throws IOException {
+        int indent = 0;
+        // <text>abcd</text> abcd是xxx标签中间的内容, 这里不能换行
+        boolean ignoreIndent = false;
+        boolean lastIsSpace = false;
+        StringBuilder buffer = new StringBuilder();
+        int index;
+        while ((index = reader.read()) >= 0) {
+            char c = (char) index;
+            if (c == '\r' || c == '\n') {
+                continue;
+            }
+            int bufSize = buffer.length();
+            char lastChar = bufSize == 0 ? '\0' : buffer.charAt(bufSize - 1);
+            if ((lastChar == '<' || lastChar == '>') && (c == ' ' || c == '\t' || c == '\f')) {
+                continue;
+            }
+            if (lastChar == '<') {
+                if (c == '/') {
+                    // </xxx> 减少缩进
+                    indent--;
+                    buffer.setLength(bufSize - 1); // 去掉最后的<
+                    if (!ignoreIndent) {
+                        appendIndent(buffer, indent); // 添加缩进
+                    }
+                    buffer.append('<').append(c);
+                } else if (c == '?') {
+                    // <?xml version="1.0" encoding="UTF-8"?>
+                    buffer.append(c);
+                } else {
+                    // <xxx> 增加缩进
+                    buffer.setLength(bufSize - 1); // 去掉最后的<
+                    appendIndent(buffer, indent); // 添加缩进
+                    buffer.append('<').append(c);
+                    indent++;
+                }
+                continue;
+            }
+            if (c == ' ' || c == '\t' || c == '\f') {
+                if (!lastIsSpace) {
+                    buffer.append(' ');
+                }
+                lastIsSpace = true;
+                continue;
+            }
+            lastIsSpace = false;
+            if (c == '<') {
+                if (lastChar == '>') {
+                    ignoreIndent = false;
+                    // 处理掉之前的内容
+                    buffer.append('\n');
+                    writer.write(buffer.toString());
+                    buffer.setLength(0);
+                } else {
+                    ignoreIndent = true;
+                }
+                // 重新开始
+                buffer.append(c);
+                // 下一步要看下一字符是什么
+                continue;
+            }
+            if (c == '>') {
+                buffer.append(c);
+                if (lastChar == '/') {
+                    indent--;
+                }
+            } else {
+                buffer.append(c);
+            }
+        }
+        if (buffer.length() > 0) {
+            writer.write(buffer.toString());
+            buffer.setLength(0);
+        }
+        writer.flush();
+    }
+
+    private static void appendIndent(StringBuilder buffer, int indent) {
+        if (indent <= 0) {
+            return;
+        }
+        for (int i = 0; i < indent; i++) {
+            buffer.append("  ");
+        }
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
index 6106a109742cd04533bc9a4c5e2e86f1867ece34..dcb94c52921ac9f933dca17428fd77685619d6ea 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/IndentTools.java
@@ -18,13 +18,45 @@ public class IndentTools {
     public static final Operator tabs = new Operator(true);
     public static final Operator space = new Operator(false);
 
-    /** 获取缩进字符 **/
+    /**
+     * 获取缩进字符
+     * @param size 缩进数量
+     * @return 缩进字符
+     * @deprecated 单词写错了
+     */
+    @Deprecated
     public static char[] getIndenTabs(int size) {
-        return getIndenTabs(size, false);
+        return getIndentTabs(size, false);
     }
 
-    /** 获取缩进字符 **/
+    /**
+     * 获取缩进字符
+     * @param size 缩进数量
+     * @param useTab 是否使用TAB字符
+     * @return 缩进字符
+     * @deprecated 单词写错了
+     */
+    @Deprecated
     public static char[] getIndenTabs(int size, boolean useTab) {
+        return getIndentTabs(size, useTab);
+    }
+
+    /**
+     * 获取缩进字符
+     * @param size 缩进数量
+     * @return 缩进字符
+     */
+    public static char[] getIndentTabs(int size) {
+        return getIndentTabs(size, false);
+    }
+
+    /**
+     * 获取缩进字符
+     * @param size 缩进数量
+     * @param useTab 是否使用TAB字符
+     * @return 缩进字符
+     */
+    public static char[] getIndentTabs(int size, boolean useTab) {
         if (size <= 0) {
             return new char[0];
         }
@@ -162,6 +194,23 @@ public class IndentTools {
 
     /** 计算空白字符等于多少缩进量(余1个空格不算缩进,余2个空格以上算1个缩进) **/
     public static int calcSpacesToTabSize(CharSequence string, int start, int end) {
+        int spaceSize = countLeadingSpaceSize(string, start, end);
+        // 余1个空格不算缩进,余2个空格算1个缩进
+        return (spaceSize + 2) / 4;
+    }
+
+    /** 计算左侧有多少个空格 **/
+    public static int countLeadingSpaceSize(CharSequence string) {
+        return countLeadingSpaceSize(string, 0, string.length());
+    }
+
+    /** 计算左侧有多少个空格 **/
+    public static int countLeadingSpaceSize(CharSequence string, int start) {
+        return countLeadingSpaceSize(string, start, string.length());
+    }
+
+    /** 计算左侧有多少个空格 **/
+    public static int countLeadingSpaceSize(CharSequence string, int start, int end) {
         int size = 0;
         int pending = 0;
         for (int i = start; i < end; i++) {
@@ -179,11 +228,17 @@ public class IndentTools {
                 break;
             }
         }
-        if (pending > 0) {
-            // 余1个空格不算缩进,余2个空格算1个缩进
-            size += (pending + 2) / 4;
-        }
-        return size;
+        return size * 4 + pending;
+    }
+
+    /**
+     * 统计开头的缩进数
+     *
+     * @param string 字符串
+     * @return 缩进数量, -1表示未找到缩进
+     */
+    public static int countLeadingIndentSize(CharSequence string) {
+        return calcSpacesToTabSize(string);
     }
 
     /**
@@ -401,7 +456,7 @@ public class IndentTools {
         }
 
         public char[] getIndenTabs(int size) {
-            return IndentTools.getIndenTabs(size, useTab);
+            return IndentTools.getIndentTabs(size, useTab);
         }
 
         /**
@@ -592,7 +647,7 @@ public class IndentTools {
          * 在最后一个换行符之后插入信息<pre>
          * 例如: \n=换行符, \t=TAB符, \s=空格
          * \n\tABC\n\t\t --> \n\tABC\n[\tMESSAGE\n]\t\tDEF // 换行模式(插入的信息与上一行缩进对齐)
-         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF      // 非换行模式
+         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF     // 非换行模式
          * \n\tABC       --> \n\tABC[\sMESSAGE\s]DEF       // 非换行模式</pre>
          * 
          * @param string 原字符串
@@ -608,7 +663,7 @@ public class IndentTools {
          * 在最后一个换行符之后插入信息<pre>
          * 例如: \n=换行符, \t=TAB符, \s=空格
          * \n\tABC\n\t\t --> \n\tABC\n[\tMESSAGE\n]\t\tDEF // 换行模式(插入的信息与上一行缩进对齐)
-         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF      // 非换行模式
+         * \n\tABC\s     --> \n\tABC\s[\sMESSAGE\s]DEF     // 非换行模式
          * \n\tABC       --> \n\tABC[\sMESSAGE\s]DEF       // 非换行模式</pre>
          * 
          * @param string 原字符串
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonFactory.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d06e734a2b2e5fbc34972a45f8250e45915dd74
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonFactory.java
@@ -0,0 +1,143 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.beans.StringWithAttrs;
+import com.gitee.qdbp.able.enums.FileErrorCode;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.json.JsonService;
+import com.gitee.qdbp.tools.files.PathTools;
+
+/**
+ * JsonService实现类查找
+ *
+ * @author zhaohuihua
+ * @version 20230126
+ */
+class JsonFactory {
+
+    private static JsonService DEFAULT_JSON_SERVICE;
+    private static ServiceException FIND_EXCEPTION;
+
+    private static final String BASE_SERVICE_CLASS = "com.gitee.qdbp.json.JsonServiceForBase";
+
+    public static boolean enabled() {
+        if (DEFAULT_JSON_SERVICE == null && FIND_EXCEPTION == null) {
+            doFindDefaultJsonService();
+        }
+        return DEFAULT_JSON_SERVICE != null;
+    }
+
+    public static void setDefaultJsonService(JsonService jsonService) {
+        VerifyTools.requireNonNull(jsonService, "jsonService");
+        DEFAULT_JSON_SERVICE = jsonService;
+        FIND_EXCEPTION = null;
+    }
+
+    public static JsonService findDefaultJsonService() {
+        if (DEFAULT_JSON_SERVICE == null && FIND_EXCEPTION == null) {
+            doFindDefaultJsonService();
+        }
+        if (FIND_EXCEPTION != null) {
+            throw FIND_EXCEPTION;
+        } else {
+            return DEFAULT_JSON_SERVICE;
+        }
+    }
+
+    private static void doFindDefaultJsonService() {
+        try {
+            Class.forName(BASE_SERVICE_CLASS);
+        } catch (ClassNotFoundException e) {
+            FIND_EXCEPTION = new ServiceException(ResultCode.SERVER_INNER_ERROR).setDetails("Missing qdbp-json.jar");
+            return;
+        }
+
+        try {
+            DEFAULT_JSON_SERVICE = findConfigJsonService();
+        } catch (ServiceException e) {
+            FIND_EXCEPTION = e;
+        } catch (Exception e) {
+            FIND_EXCEPTION = new ServiceException(ResultCode.SERVER_INNER_ERROR, e);
+        }
+    }
+
+    private static synchronized JsonService findConfigJsonService() {
+        String configPath = "settings/global/json-service.yml";
+        List<URL> urls = PathTools.scanResources(configPath);
+
+        URL url = urls.get(0);
+        String string;
+        String path = PathTools.getJarRelativePath(url);
+        try {
+            string = PathTools.downloadString(url);
+        } catch (IOException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(path);
+        }
+        Map<String, String> roots = RuleTools.splitRuleKeyValues(string).asMap();
+        String rootValue = roots.get("json-service");
+        if (VerifyTools.isBlank(rootValue)) {
+            throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR)
+                    .setDetails("json-service config missing --> " + path);
+        }
+        List<StringWithAttrs> contents = RuleTools.splitRuleWithAttrs(rootValue);
+        if (contents.isEmpty()) {
+            throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR)
+                    .setDetails("json-service config error --> " + path);
+        }
+        List<String> names = new ArrayList<>();
+        for (StringWithAttrs content : contents) {
+            String name = StringTools.trimRight(content.getString(), ':', ' ');
+            if (VerifyTools.isBlank(content.getAttrs())) {
+                String msg = "json-service.{} config error --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, name, path);
+            }
+            Map<String, String> attrs = content.getAttrs().asMap();
+            String serviceType = attrs.get("serviceClass");
+            String requiredType = attrs.get("requiredClass");
+            if (VerifyTools.isBlank(serviceType)) {
+                String msg = "json-service.{} missing 'serviceClass' --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, name, path);
+            }
+            if (VerifyTools.isBlank(requiredType)) {
+                String msg = "json-service.{} missing 'requiredClass' --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, name, path);
+            }
+            try {
+                Class.forName(requiredType);
+            } catch (ClassNotFoundException ignore) {
+                // requiredClass不在jvm当中
+                names.add(name);
+                continue;
+            }
+            Class<?> serviceClass;
+            try {
+                serviceClass = Class.forName(serviceType);
+            } catch (ClassNotFoundException e) {
+                // serviceClass不在jvm当中
+                String msg = "json-service.{}.serviceClass not found: {} --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR, e).setDetails(msg, name, serviceType, path);
+            }
+            if (!JsonService.class.isAssignableFrom(serviceClass)) {
+                // serviceClass不是JsonService的子类
+                String msg = "json-service.{}.serviceClass is not implements JsonService: {} --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, name, serviceType, path);
+            }
+            try {
+                return (JsonService) serviceClass.newInstance();
+            } catch (Exception e) {
+                // 初始化服务类失败
+                String msg = "json-service.{}.serviceClass new instance error: {} --> {}";
+                throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR, e).setDetails(msg, name, serviceType, path);
+            }
+        }
+        // 所有requiredClass都不在jvm当中
+        String msg = "JsonService instance not found for [{}] --> {}";
+        String traces = ConvertTools.joinToString(names);
+        throw new ServiceException(ResultCode.SYSTEM_CONFIG_ERROR).setDetails(msg, traces, path);
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonMaps.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonMaps.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd1b7b3a794d8e0df03e69d18b2636b053cb7d71
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonMaps.java
@@ -0,0 +1,114 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Map工具类
+ *
+ * @author zhaohuihua
+ * @version 20230126
+ */
+class JsonMaps extends JsonFactory {
+
+    /**
+     * 获取Map中的字符串值, 并转换为字符串
+     *
+     * @param map Map
+     * @param key KEY
+     * @return 字符串
+     * @deprecated 改为 {@linkplain MapTools#getString(Map, String)}
+     */
+    @Deprecated
+    public static String getMapString(Map<String, Object> map, String key) {
+        return MapTools.getString(map, key);
+    }
+
+    /**
+     * 获取Map中的值, 并转换为指定类型
+     *
+     * @param map Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 指定类型的值
+     * @deprecated 改为 {@linkplain MapTools#getValue(Map, String, Class)}
+     */
+    @Deprecated
+    public static <T> T getMapValue(Map<String, Object> map, String key, Class<T> clazz) {
+        return MapTools.getValue(map, key, clazz);
+    }
+
+    /**
+     * 获取Map中的值, 并转换为指定类型
+     *
+     * @param map Map
+     * @param key KEY
+     * @param defaults 默认值
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 指定类型的值
+     * @deprecated 改为 {@linkplain MapTools#getValue(Map, String, Object, Class)}
+     */
+    @Deprecated
+    public static <T> T getMapValue(Map<String, Object> map, String key, T defaults, Class<T> clazz) {
+        return MapTools.getValue(map, key, defaults, clazz);
+    }
+
+    /**
+     * 获取Map中的列表值
+     *
+     * @param map Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 列表
+     * @deprecated 改为 {@linkplain MapTools#getList(Map, String, Class)}
+     */
+    @Deprecated
+    public static <T> List<T> getMapValues(Map<String, Object> map, String key, Class<T> clazz) {
+        return MapTools.getList(map, key, clazz);
+    }
+
+    /**
+     * 获取Map中的列表值
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @param clazz 目标类型
+     * @param <T> 目标类型
+     * @return 列表
+     * @deprecated 改为 {@linkplain MapTools#getList(Map, String, Class)}
+     */
+    @Deprecated
+    public static <T> List<T> getMapValues(Map<String, Object> map, String key, boolean nullToEmpty, Class<T> clazz) {
+        return MapTools.getList(map, key, clazz);
+    }
+
+    /**
+     * 获取Map中的子Map
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @return Map
+     * @deprecated 改为 {@linkplain MapTools#getSubMap(Map, String)}
+     */
+    @Deprecated
+    public static Map<String, Object> getMapSubMap(Map<String, Object> map, String key) {
+        return MapTools.getSubMap(map, key);
+    }
+
+    /**
+     * 获取Map中的子Map列表
+     *
+     * @param map 原Map
+     * @param key KEY
+     * @return Map列表
+     * @deprecated 改为 {@linkplain MapTools#getSubMaps(Map, String)}
+     */
+    @Deprecated
+    public static List<Map<String, Object>> getMapSubMaps(Map<String, Object> map, String key) {
+        return MapTools.getSubMaps(map, key);
+    }
+
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca02ac1d8cde4b4c6ee9864e937d3df0f6503213
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
@@ -0,0 +1,296 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import com.gitee.qdbp.json.JsonFeature;
+
+/**
+ * Json工具类<br>
+ * 必须引入qdbp-json.jar, 且必须有fastjson/jackson/gson三个json基础jar之一
+ *
+ * @author zhaohuihua
+ * @version 180621
+ */
+public class JsonTools extends JsonMaps {
+
+    /** 静态工具类私有构造方法 **/
+    private JsonTools() {
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T convert(Object object, Class<T> clazz) {
+        if (object == null) {
+            return convertNullToPrimitive(clazz);
+        }
+        if (clazz.isAssignableFrom(object.getClass())) {
+            return (T) object;
+        }
+        if (object instanceof Number) {
+            return convertNumberValue((Number) object, clazz);
+        }
+        if (object instanceof String) {
+            return convertStringValue((String) object, clazz);
+        }
+        return findDefaultJsonService().convert(object, clazz);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T convertNullToPrimitive(Class<T> clazz) {
+        if (clazz == boolean.class) {
+            return (T) Boolean.FALSE;
+        } else if (clazz == int.class) {
+            return (T) Integer.valueOf(0);
+        } else if (clazz == long.class) {
+            return (T) Long.valueOf(0);
+        } else if (clazz == double.class) {
+            return (T) Double.valueOf(0);
+        } else if (clazz == short.class) {
+            return (T) Short.valueOf((short) 0);
+        } else if (clazz == byte.class) {
+            return (T) Byte.valueOf((byte) 0);
+        } else if (clazz == float.class) {
+            return (T) Float.valueOf(0);
+        } else {
+            return null;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T convertNumberValue(Number number, Class<T> clazz) {
+        if (clazz == int.class || clazz == Integer.class) {
+            return (T) Integer.valueOf(number.intValue());
+        } else if (clazz == long.class || clazz == Long.class) {
+            return (T) Long.valueOf(number.longValue());
+        } else if (clazz == double.class || clazz == Double.class) {
+            return (T) Double.valueOf(number.doubleValue());
+        } else if (clazz == short.class || clazz == Short.class) {
+            return (T) Short.valueOf(number.shortValue());
+        } else if (clazz == byte.class || clazz == Byte.class) {
+            return (T) Byte.valueOf(number.byteValue());
+        } else if (clazz == float.class || clazz == Float.class) {
+            return (T) Float.valueOf(number.floatValue());
+        } else if (clazz == BigDecimal.class) {
+            return (T) new BigDecimal(String.valueOf(number));
+        } else if (clazz == BigInteger.class) {
+            return (T) new BigInteger(String.valueOf(number));
+        } else if (clazz == AtomicInteger.class) {
+            return (T) new AtomicInteger(number.intValue());
+        } else if (clazz == AtomicLong.class) {
+            return (T) new AtomicLong(number.longValue());
+        } else {
+            return findDefaultJsonService().convert(number, clazz);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T convertStringValue(String string, Class<T> clazz) {
+        if (clazz == Date.class) {
+            return (T) DateTools.parse(string);
+        }
+        if (clazz == int.class || clazz == Integer.class) {
+            return (T) Integer.valueOf(ConvertTools.toInteger(string));
+        }
+        if (clazz == long.class || clazz == Long.class) {
+            return (T) Long.valueOf(ConvertTools.toLong(string));
+        }
+        if (clazz == double.class || clazz == Double.class) {
+            return (T) Double.valueOf(ConvertTools.toDouble(string));
+        }
+        if (clazz == float.class || clazz == Float.class) {
+            return (T) Float.valueOf(ConvertTools.toFloat(string));
+        }
+        if (clazz == boolean.class || clazz == Boolean.class) {
+            return (T) Boolean.valueOf(ConvertTools.toBoolean(string));
+        }
+        if (VerifyTools.isJsonObjectString(string)) {
+            if (clazz == Map.class) {
+                return (T) parseAsMap(string);
+            }
+            if (clazz == HashMap.class) {
+                return (T) new HashMap<>(parseAsMap(string));
+            }
+            if (!ReflectTools.isPrimitive(clazz, false)
+                    && !clazz.isArray() && !Collection.class.isAssignableFrom(clazz)) {
+                Map<String, Object> map = parseAsMap(string);
+                return mapToBean(map, clazz);
+            }
+        }
+        return findDefaultJsonService().convert(string, clazz);
+    }
+
+    /** 对象转换为字符串 **/
+    public static String toJsonString(Object object) {
+        return findDefaultJsonService().toJsonString(object);
+    }
+
+    /** 对象转换为字符串 **/
+    public static String toJsonString(Object object, JsonFeature.Serialization feature) {
+        return findDefaultJsonService().toJsonString(object, feature);
+    }
+
+    private static final JsonFeature.Serialization SIMPLIFY_FEATURE = JsonFeature.serialization()
+            .setQuoteFieldNames(false)
+            .setWriteFieldNullValue(false)
+            .setWriteMapNullValue(false)
+            .lockToUnmodifiable();
+
+    /** 转换为简化的字符串, json的key不会有引号, 可以稍微简化一下结构 **/
+    // 目前与toLogString相同, 但以后toLogString有可能会更加简化 (因为log不用考虑反序列化)
+    public static String toSimpleString(Object object) {
+        return findDefaultJsonService().toJsonString(object, SIMPLIFY_FEATURE);
+    }
+
+    /** 转换为日志字符串, 有可能无法反序列化, json的key不会有引号, 可以稍微简化一下结构 **/
+    public static String toLogString(Object object) {
+        return findDefaultJsonService().toJsonString(object, SIMPLIFY_FEATURE);
+    }
+
+    /** 解析Json字符串对象 **/
+    public static Map<String, Object> parseAsMap(String jsonString) {
+        return findDefaultJsonService().parseAsMap(jsonString);
+    }
+
+    /** 解析Json字符串对象 **/
+    public static Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature) {
+        return findDefaultJsonService().parseAsMap(jsonString, feature);
+    }
+
+    /** 解析Json字符串数组 **/
+    public static List<Map<String, Object>> parseAsMaps(String jsonString) {
+        return findDefaultJsonService().parseAsMaps(jsonString);
+    }
+
+    /** 解析Json字符串数组 **/
+    public static List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature) {
+        return findDefaultJsonService().parseAsMaps(jsonString, feature);
+    }
+
+    /** 解析Json字符串对象 **/
+    public static <T> T parseAsObject(String jsonString, Class<T> clazz) {
+        return findDefaultJsonService().parseAsObject(jsonString, clazz);
+    }
+
+    /** 解析Json字符串对象 **/
+    public static <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        return findDefaultJsonService().parseAsObject(jsonString, clazz, feature);
+    }
+
+    /** 解析Json字符串数组 **/
+    public static <T> List<T> parseAsObjects(String jsonString, Class<T> clazz) {
+        return findDefaultJsonService().parseAsObjects(jsonString, clazz);
+    }
+
+    /** 解析Json字符串数组 **/
+    public static <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        return findDefaultJsonService().parseAsObjects(jsonString, clazz, feature);
+    }
+
+    /**
+     * 将Map内容设置到Java对象中
+     *
+     * @param map Map
+     * @param bean 目标Java对象
+     */
+    public static void mapFillToBean(Map<String, ?> map, Object bean) {
+        findDefaultJsonService().mapFillToBean(map, bean);
+    }
+
+    /**
+     * 将Map转换为Java对象
+     *
+     * @param <T> 目标类型
+     * @param map Map
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    public static <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
+        return findDefaultJsonService().mapToBean(map, clazz);
+    }
+
+    /**
+     * 将Java对象转换为Map
+     *
+     * @param bean JavaBean对象
+     * @return Map
+     */
+    public static Map<String, Object> beanToMap(Object bean) {
+        return findDefaultJsonService().beanToMap(bean);
+    }
+
+    /**
+     * 将Java对象转换为Map
+     *
+     * @param bean Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map
+     */
+    public static Map<String, Object> beanToMap(Object bean, boolean deep, boolean clearBlankValue) {
+        return findDefaultJsonService().beanToMap(bean, deep, clearBlankValue);
+    }
+
+    /**
+     * 将Map数组转换为Java对象列表
+     *
+     * @param <T> 目标类型
+     * @param maps Map数组
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    public static <T, V> List<T> mapToBeans(Collection<Map<String, V>> maps, Class<T> clazz) {
+        return findDefaultJsonService().mapToBeans(maps, clazz);
+    }
+
+    /**
+     * 将Java对象转换为Map列表<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param beans JavaBean对象列表
+     * @return Map列表
+     */
+    public static List<Map<String, Object>> beanToMaps(Collection<?> beans) {
+        return findDefaultJsonService().beanToMaps(beans);
+    }
+
+    /**
+     * 将Java对象转换为Map列表
+     *
+     * @param beans Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map列表
+     */
+    public static List<Map<String, Object>> beanToMaps(Collection<?> beans, boolean deep, boolean clearBlankValue) {
+        return findDefaultJsonService().beanToMaps(beans, deep, clearBlankValue);
+    }
+
+    /**
+     * 将对象转换为以换行符分隔的日志文本<br>
+     * 如 newlineLogs(params, operator) 返回 \n\t{paramsJson}\n\t{operatorJson}<br>
+     *
+     * @param objects 对象
+     * @return 日志文本
+     */
+    public static String newlineLogs(Object... objects) {
+        StringBuilder buffer = new StringBuilder();
+        for (Object object : objects) {
+            buffer.append("\n\t");
+            if (object == null) {
+                buffer.append("null");
+            } else if (object instanceof String) {
+                buffer.append(object);
+            } else {
+                buffer.append(object.getClass().getSimpleName()).append(": ");
+                buffer.append(toLogString(object));
+            }
+        }
+        return buffer.toString();
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
index 37a5b9ac2508208cedef7a2e5b986534fcd1b1d6..7124c3eeda7f4ea9fbf7732ba3f3b8d713047bce 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/MapTools.java
@@ -7,10 +7,16 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import com.gitee.qdbp.able.beans.DepthMap;
+import com.gitee.qdbp.able.beans.LinkedListMap;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.model.reusable.ExpressionMap;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.beans.MapPlus;
 
 /**
  * Map工具类<br>
@@ -26,38 +32,204 @@ public class MapTools {
     private MapTools() {
     }
 
+    /**
+     * 将Map转换为JsonMap (key转换为String)<br>
+     * 如果map本就是以String为Key, 则返回原对象
+     *
+     * @param map Map对象
+     * @return JsonMap
+     * @since 5.5.0
+     */
+    public static Map<String, Object> toJsonMap(Map<?, ?> map) {
+        Map<String, Object> result = newMap(map);
+        if (map == null) {
+            return result;
+        }
+        boolean changes = false;
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            if (entry.getKey() == null || entry.getKey() instanceof String) {
+                result.put((String) entry.getKey(), entry.getValue());
+            } else {
+                result.put(entry.getKey().toString(), entry.getValue());
+                changes = true;
+            }
+        }
+        if (changes) {
+            return result;
+        } else {
+            // 如果完全未做转换, 则返回原对象
+            @SuppressWarnings("unchecked")
+            Map<String, Object> older = (Map<String, Object>) map;
+            return older;
+        }
+    }
+
+    private static Map<String, Object> newMap(Map<?, ?> old) {
+        if (old instanceof LinkedHashMap) {
+            return new LinkedHashMap<>();
+        } else {
+            return new HashMap<>();
+        }
+    }
+
+    /**
+     * 将对象列表转换为JsonMap列表
+     *
+     * @param list 列表
+     * @return JsonMap列表
+     * @since 5.5.0
+     */
+    public static List<Map<String, Object>> toJsonMaps(Collection<?> list) {
+        return toJsonMaps(list, true);
+    }
+
+    /**
+     * 将对象列表转换为JsonMap列表
+     *
+     * @param list 列表
+     * @return JsonMap列表
+     * @since 5.5.0
+     */
+    private static List<Map<String, Object>> toJsonMaps(Collection<?> list, boolean throwOnError) {
+        List<Map<String, Object>> results = new ArrayList<>();
+        int i = -1;
+        boolean changes = !(list instanceof List);
+        for (Object item : list) {
+            i++;
+            if (item == null) {
+                results.add(null);
+            } else if (item instanceof Map) {
+                Map<String, Object> newer = toJsonMap((Map<?, ?>) item);
+                results.add(newer);
+                if (item != newer) {
+                    changes = true;
+                }
+            } else {
+                results.add(parseListItem(item, i, throwOnError));
+                changes = true;
+            }
+        }
+        if (changes) {
+            return results;
+        } else {
+            // 如果完全未做转换, 则返回原对象
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> older = (List<Map<String, Object>>) list;
+            return older;
+        }
+    }
+
+    private static Map<String, Object> parseListItem(Object item, int i, boolean throwOnError) {
+        if (ReflectTools.isPrimitive(item.getClass(), false)) {
+            if (!throwOnError) {
+                return null;
+            } else {
+                String msg = "[" + i + "] " + item.getClass().getSimpleName() + " can't convert to map.";
+                throw new ServiceException(ResultCode.SERVER_INNER_ERROR).setDetails(msg);
+            }
+        }
+        if (!throwOnError && !JsonTools.enabled()) {
+            return null;
+        }
+        try {
+            return JsonTools.beanToMap(item, true, false);
+        } catch (ServiceException e) {
+            if (!throwOnError) {
+                return null;
+            } else {
+                if (e.getDetails() == null) {
+                    e.setDetails("[" + i + "] " + item.getClass().getSimpleName() + " can't convert to map.");
+                } else {
+                    e.setDetails("[" + i + "] " + e.getDetails());
+                }
+                throw e;
+            }
+        } catch (Exception e) {
+            if (!throwOnError) {
+                return null;
+            } else {
+                throw new ServiceException(ResultCode.SERVER_INNER_ERROR)
+                        .setDetails("[" + i + "] " + e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 将Map的KEY转换为驼峰名
+     *
+     * @param data 数据列表
+     * @since 5.5.2
+     */
+    public static void mapKeyToCamel(Map<String, Object> data) {
+        if (data == null) {
+            return;
+        }
+        Map<String, Object> temp = newMap(data);
+        for (Map.Entry<String, Object> entry : data.entrySet()) {
+            String key = entry.getKey();
+            temp.put(NamingTools.toCamelString(key), entry.getValue());
+        }
+        data.clear();
+        data.putAll(temp);
+    }
+
+    /**
+     * 将Map的KEY转换为驼峰名
+     *
+     * @param data 数据列表
+     * @since 5.5.2
+     */
+    public static void mapKeyToCamel(List<Map<String, Object>> data) {
+        if (data == null) {
+            return;
+        }
+        for (Map<String, Object> item : data) {
+            mapKeyToCamel(item);
+        }
+    }
+
     /**
      * 清除Map中的空值
-     * 
+     *
      * @param map Map对象
      */
-    @SuppressWarnings("unchecked")
-    public static void clearBlankValue(Map<String, Object> map) {
+    public static void clearBlankValue(Map<?, ?> map) {
         if (map == null) {
             return;
         }
-        Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
+        Iterator<? extends Map.Entry<?, ?>> iterator = map.entrySet().iterator();
         while (iterator.hasNext()) {
-            Map.Entry<String, Object> entry = iterator.next();
+            Map.Entry<?, ?> entry = iterator.next();
             Object value = entry.getValue();
             if (VerifyTools.isBlank(value)) {
                 iterator.remove();
             } else if (value instanceof Map) {
-                clearBlankValue((Map<String, Object>) value);
+                clearBlankValue((Map<?, ?>) value);
             } else if (value instanceof Collection) {
                 Collection<?> collection = (Collection<?>) value;
                 for (Object item : collection) {
                     if (item instanceof Map) {
-                        clearBlankValue((Map<String, Object>) item);
+                        clearBlankValue((Map<?, ?>) item);
                     }
                 }
             }
         }
     }
 
+    /**
+     * 解析类似Json的参数: name:zhh;phone:139xxxx8888
+     *
+     * @param params 类似Json的参数
+     * @return Map
+     */
+    public static Map<String, String> parseJsonLikeParams(String params) {
+        List<String> array = StringTools.splits(params, true, true, ';', '\n');
+        return parseStringParams(array, ':', null);
+    }
+
     /**
      * 解析请求参数: name=zhh&phone=139xxxx8888
-     * 
+     *
      * @param params 请求参数
      * @return Map
      */
@@ -66,19 +238,34 @@ public class MapTools {
     }
 
     /**
-     * 解析请求参数
-     * 
+     * 解析请求参数: name=zhh&phone=139xxxx8888&code=&amp;amp;
+     *
      * @param params 请求参数
      * @param charset 字符编码: 如果不为空, 将对value执行URLDecoder.decode(value, charset)
      * @return Map
      */
     public static Map<String, String> parseRequestParams(String params, String charset) {
+        List<String> array = splitParams(params);
+        return parseStringParams(array, '=', charset);
+    }
+
+    /**
+     * 解析字符串参数: name=zhh&phone=139xxxx8888
+     *
+     * @param params 请求参数
+     * @param separator 分隔符
+     * @param charset 字符编码: 如果不为空, 将对value执行URLDecoder.decode(value, charset)
+     * @return Map
+     */
+    private static Map<String, String> parseStringParams(List<String> params, char separator, String charset) {
         Map<String, String> map = new HashMap<>();
-        String[] array = StringTools.split(params, '&');
-        for (String item : array) {
+        if (params == null) {
+            return map;
+        }
+        for (String item : params) {
             // 不能用StringTools.split('=');
             // 因为有可能存在多个等号: contentType=application/json;charset=UTF-8
-            int index = item.indexOf('=');
+            int index = item.indexOf(separator);
             if (index < 0) {
                 map.put(item, null);
             } else if (index == 0) {
@@ -98,9 +285,108 @@ public class MapTools {
         return map;
     }
 
+    private static List<String> splitParams(String string) {
+        if (string == null || string.length() == 0) {
+            return new ArrayList<>();
+        }
+        List<String> list = new ArrayList<>();
+        StringBuilder buffer = new StringBuilder();
+        boolean lastIsSplitChar = false;
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (c == '\\' && i < z - 1) {
+                char next = string.charAt(++i);
+                if (next == '&' || next == '\\') {
+                    // 是转义字符 (只加下一字符, 吞掉斜杠)
+                    buffer.append(next);
+                    lastIsSplitChar = false;
+                    continue;
+                } else {
+                    // 斜杠后面不是转义字符, 补上斜杠, 继续处理下一字符
+                    buffer.append(c);
+                    c = next;
+                }
+            }
+            if (c == '&') {
+                // 判断是不是实体名称
+                String entityName = findEntityName(string, i + 1);
+                if (entityName != null) {
+                    buffer.append(c).append(entityName);
+                    lastIsSplitChar = false;
+                    i += entityName.length();
+                    continue;
+                }
+                // 遇到分隔符
+                list.add(StringTools.trim(buffer.toString()));
+                buffer.setLength(0);
+                lastIsSplitChar = true;
+            } else {
+                // 不是分隔符
+                buffer.append(c);
+                lastIsSplitChar = false;
+            }
+        }
+        if (buffer.length() > 0) {
+            list.add(StringTools.trim(buffer.toString()));
+            buffer.setLength(0);
+        }
+        if (lastIsSplitChar) {
+            list.add("");
+        }
+        return list;
+    }
+
+    private static String findEntityName(String string, int index) {
+        StringBuilder temp = new StringBuilder();
+        for (int i = index, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (temp.length() == 0 && c == '#') {
+                temp.append(c);
+            } else if (c == '$') {
+                // isWordChar('$')返回true,但这里不支持$,所以单独处理
+                break;
+            } else if (StringTools.isWordChar(c)) {
+                temp.append(c);
+            } else if (c == ';') {
+                temp.append(c);
+                return temp.toString();
+            } else {
+                break;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 解析请求参数
+     *
+     * @param params 请求参数, request.getParameterMap()
+     * @return Map
+     */
+    public static Map<String, Object> parseRequestMap(Map<String, String[]> params) {
+        Map<String, Object> map = new HashMap<>();
+        if (params == null) {
+            return map;
+        }
+
+        for (Map.Entry<String, String[]> entry : params.entrySet()) {
+            String key = entry.getKey();
+            String[] values = entry.getValue();
+            if (key == null || key.length() == 0 || values == null || values.length == 0) {
+                continue;
+            }
+            if (values.length == 1) {
+                map.put(key, values[0]);
+            } else {
+                map.put(key, values);
+            }
+        }
+        return map;
+    }
+
     /**
      * 判断Map中是否存在指定的KEY
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @return 是否存在
@@ -108,7 +394,7 @@ public class MapTools {
     public static boolean containsKey(Map<?, ?> map, String key) {
         if (key == null || map.containsKey(key)) {
             return map.containsKey(key);
-        } else if (key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0) {
+        } else if (isComplexKey(key)) {
             return ReflectTools.containsDepthFields(map, key);
         } else {
             return false;
@@ -116,29 +402,32 @@ public class MapTools {
     }
 
     /**
-     * 获取字符串类型的Map值<br>
-     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getString("xxx")将返回null
-     * 
+     * 获取字符串类型的Map值
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @return 字符串
      */
     public static String getString(Map<?, ?> map, String key) {
         Object value = getValue(map, key);
-        return value instanceof String ? (String) value : null;
+        return value == null ? null : value.toString();
     }
 
     /**
      * 获取Map值
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @return Map值
      */
     public static Object getValue(Map<?, ?> map, String key) {
         if (key == null || map.containsKey(key)) {
-            return map.get(key);
-        } else if (key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0) {
+            if (map instanceof ExpressionMap) {
+                return ((ExpressionMap) map).getOriginal(key);
+            } else {
+                return map.get(key);
+            }
+        } else if (isComplexKey(key)) {
             return ReflectTools.getDepthValue(map, key);
         } else {
             return null;
@@ -146,160 +435,246 @@ public class MapTools {
     }
 
     /**
-     * 获取指定类型的Map值<br>
-     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getValue("xxx", Boolean.class)将返回null
-     * 
+     * 获取指定类型的Map值
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
-     * @return Map值
+     * @return Map值, 类型不匹配时返回null
      */
-    @SuppressWarnings("unchecked")
     public static <T> T getValue(Map<?, ?> map, String key, Class<T> clazz) {
         Object value = getValue(map, key);
-        return value != null && clazz.isAssignableFrom(value.getClass()) ? (T) value : null;
+        return convertValue(value, null, clazz);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T convertValue(Object value, T defaults, Class<T> clazz) {
+        if (value == null && defaults != null) {
+            return defaults;
+        } else {
+            if (clazz == Map.class) {
+                List<Object> objects = ConvertTools.parseList(value);
+                if (objects.size() == 1 && objects.get(0) == value) {
+                    // 如果列表容器只有value一个元素, 说明value不是列表
+                    return (T) toJsonMap((Map<?, ?>) value);
+                } else {
+                    // 如果结果是数组, 包装成Map返回
+                    return (T) new LinkedListMap(JsonTools.beanToMaps(objects, true, false));
+                }
+            } else {
+                try {
+                    T result = JsonTools.convert(value, clazz);
+                    return result != null ? result : defaults;
+                } catch (Exception e) {
+                    return defaults;
+                }
+            }
+        }
     }
 
     /**
-     * 获取指定类型的Map值<br>
-     * 注意: 该方法不进行类型转换, 如果map中存在xxx=100, ConvertTools.getValue("xxx", Boolean.class)将返回null
-     * 
+     * 获取指定类型的Map值
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param defaults 值不存在或值类型不匹配时返回的默认值
      * @param clazz 指定类型
      * @return Map值
      */
-    @SuppressWarnings("unchecked")
     public static <T> T getValue(Map<?, ?> map, String key, T defaults, Class<T> clazz) {
         Object value = getValue(map, key);
-        return value != null && clazz.isAssignableFrom(value.getClass()) ? (T) value : defaults;
+        return convertValue(value, defaults, clazz);
     }
 
     /**
-     * 获取指定类型的Map值列表<br>
-     * 注意: 该方法不进行类型转换, 如果key指向的对象不是Iterable, 将返回null<br>
-     * 注意: 如果Iterable中存在类型不匹配的元素, 也会返回null
-     * 
+     * 获取指定类型的Map值列表, 值不存在时将返回空List
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param clazz 指定类型
-     * @return Map值列表, 值不存在或值类型不匹配时返回null
+     * @return Map值列表, 值不存在将返回空List, 值类型不匹配时List项将返回null
      */
     public static <T> List<T> getList(Map<?, ?> map, String key, Class<T> clazz) {
-        return getList(map, key, false, clazz);
+        Object value = getValue(map, key);
+        if (value == null) {
+            return new ArrayList<>();
+        }
+        List<Object> list = ConvertTools.parseList(value);
+        List<T> result = new ArrayList<>();
+        for (Object item : list) {
+            result.add(convertValue(item, null, clazz));
+        }
+        return result;
     }
 
     /**
-     * 获取指定类型的Map值列表<br>
-     * 注意: 该方法不进行类型转换, 如果key指向的对象不是Iterable且不是clazz对象, 将返回null或空列表<br>
-     * 注意: 如果Iterable中存在类型不匹配的元素, 也会返回null或空列表
-     * 
+     * 获取指定类型的Map值列表, 值不存在时将返回空List
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0].telphone
      * @param nullToEmpty 值不存在或值类型不匹配时是否返回空列表
      * @param clazz 指定类型
      * @return Map值列表
+     * @deprecated 改为 {@linkplain #getList(Map, String, Class)}
      */
-    @SuppressWarnings("unchecked")
+    @Deprecated
     public static <T> List<T> getList(Map<?, ?> map, String key, boolean nullToEmpty, Class<T> clazz) {
-        Object value = getValue(map, key);
-        if (value == null) {
-            return nullToEmpty ? new ArrayList<T>() : null;
-        } else if (!(value instanceof Iterable)) {
-            if (clazz.isAssignableFrom(value.getClass())) {
-                List<T> list = new ArrayList<>();
-                list.add((T) value);
-                return list;
-            } else {
-                return nullToEmpty ? new ArrayList<T>() : null;
-            }
-        }
-        Iterable<?> collection = (Iterable<?>) value;
-        List<T> list = new ArrayList<>();
-        for (Object item : collection) {
-            if (item == null) {
-                list.add(null);
-            } else if (clazz.isAssignableFrom(item.getClass())) {
-                list.add((T) item);
-            } else {
-                return nullToEmpty ? new ArrayList<T>() : null; // 存在类型不匹配的元素, 返回空对象
-            }
-        }
-        return list;
+        return getList(map, key, clazz);
     }
 
     /**
      * 获取子Map
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0]
      * @return 子Map
      */
     public static Map<String, Object> getSubMap(Map<?, ?> map, String key) {
-        return getSubMap(map, key, false);
+        Map<?, ?> value = getValue(map, key, Map.class);
+        return toJsonMap(value);
     }
 
     /**
      * 获取子Map
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses[0]
      * @param nullToEmpty 值不存在或值类型不匹配时是否返回空Map
      * @return 子Map
+     * @deprecated 改为 {@linkplain #getSubMap(Map, String)}
      */
-    @SuppressWarnings("unchecked")
+    @Deprecated
     public static Map<String, Object> getSubMap(Map<?, ?> map, String key, boolean nullToEmpty) {
-        Map<String, Object> result = getValue(map, key, Map.class);
-        return result == null && nullToEmpty ? new HashMap<String, Object>() : result;
+        return getSubMap(map, key);
     }
 
     /**
      * 获取子Map数组
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses
-     * @return 子Map数组
+     * @return 子Map数组, 值不存在将返回空List, 值不能转换为Map时List项将返回null
      */
     public static List<Map<String, Object>> getSubMaps(Map<?, ?> map, String key) {
-        return getSubMaps(map, key, false);
+        Object value = getValue(map, key);
+        if (value == null) {
+            return new ArrayList<>();
+        }
+        return toJsonMaps(ConvertTools.parseList(value), false);
     }
 
     /**
      * 获取子Map数组
-     * 
+     *
      * @param map Map容器
      * @param key KEY, 支持多级, 如user.addresses
      * @param nullToEmpty 值不存在或值类型不匹配时是否返回空列表
      * @return 子Map数组
+     * @deprecated 改为 {@linkplain #getSubMaps(Map, String)}
      */
-    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Deprecated
     public static List<Map<String, Object>> getSubMaps(Map<?, ?> map, String key, boolean nullToEmpty) {
-        List<Map> maps = getList(map, key, nullToEmpty, Map.class);
-        if (maps == null) {
-            return null;
+        return getSubMaps(map, key);
+    }
+
+    /**
+     * 深度设置Map值<br>
+     * a.b.c, a.b.c.d 不能共存, 保留路径最深的a.b.c.d
+     *
+     * <pre>
+     * Map&lt;String, Object&gt; map = new HashMap&lt;&gt;();
+     * MapTools.putValue(map, "author", "xxx"); // 该设置无效, 会被后面的覆盖掉
+     * MapTools.putValue(map, "author.code", 100);
+     * MapTools.putValue(map, "author.name", "zhaohuihua");
+     *
+     * MapTools.putValue(map, "code.folder.service", "service");
+     * MapTools.putValue(map, "code.folder.page", "views");
+     * MapTools.putValue(map, "code.folder", "java"); // 该设置会将上面两行覆盖掉
+     * </pre>
+     *
+     * @param map Map容器
+     * @param key KEY, 支持多级, 如user.addresses
+     * @param value 值
+     */
+    @SuppressWarnings("unchecked")
+    public static Object putValue(Map<String, Object> map, String key, Object value) {
+        if (key == null || key.indexOf('.') <= 0) {
+            return map.put(key, value);
         }
-        // Cannot cast from List<Map> to List<Map<String,Object>>
-        // return (List<Map<String, Object>>) maps;
-        List<Map<String, Object>> result = new ArrayList<>();
-        for (Map item : maps) {
-            result.add(item);
+
+        List<String> keys = StringTools.splits(key, '.');
+        Iterator<String> iterator = keys.iterator();
+
+        Map<String, Object> parent = map;
+        while (parent != null && iterator.hasNext()) {
+            String name = iterator.next();
+
+            Object older = parent.get(name);
+            if (iterator.hasNext()) {
+                // 有下一级
+                if (older instanceof Map) {
+                    parent = (Map<String, Object>) older;
+                } else {
+                    // 如果之前有a.b.c, 新加一个a.b.c.d, 则用map覆盖掉a.b.c
+                    Map<String, Object> newer = new HashMap<>();
+                    parent.put(name, newer);
+                    parent = newer;
+                }
+            } else {
+                // 没有下一级
+                return parent.put(name, value);
+            }
         }
-        return result;
+        return null;
+    }
+
+    /**
+     * 删除map中的key
+     *
+     * @param map 原Map
+     * @param key KEY, 支持多级, 如user.addresses[0].telphone
+     * @return 被删除的对象
+     */
+    public static Object removeKey(Map<String, Object> map, String key) {
+        if (key == null || key.indexOf('.') <= 0 || map.containsKey(key)) {
+            return map.remove(key);
+        }
+
+        List<String> keys = StringTools.splits(key, '.');
+        Iterator<String> iterator = keys.iterator();
+
+        Map<?, ?> parent = map;
+        while (iterator.hasNext()) {
+            String name = iterator.next();
+
+            Object older = parent.get(name);
+            if (iterator.hasNext()) {
+                // 有下一级
+                if (older instanceof Map) {
+                    parent = (Map<?, ?>) older;
+                } else {
+                    break;
+                }
+            } else {
+                // 没有下一级
+                return parent.remove(name);
+            }
+        }
+        return null;
     }
 
     /**
      * 过滤数据字段<br>
      * <pre>
-        Map&lt;String, Object&gt; list = [ { main:{}, detail:{}, subject:{}, finance:[], target:[] } ];
-        String mainFields = "main:id,projectName,publishStatus";
-        String detailFields = "detail:id,totalAmount,holdingTime";
-        String subjectFields = "subject:id,customerName,industryCategory";
-        String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
-        String targetFields = "target:id,industryCategory,latestMkt,latestPer";
-        MapTools.filterFields(list, mainFields, detailFields, subjectFields, financeFields, targetFields);
+     * Map&lt;String, Object&gt; list = [ { main:{}, detail:{}, subject:{}, finance:[], target:[] } ];
+     * String mainFields = "main:id,projectName,publishStatus";
+     * String detailFields = "detail:id,totalAmount,holdingTime";
+     * String subjectFields = "subject:id,customerName,industryCategory";
+     * String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
+     * String targetFields = "target:id,industryCategory,latestMkt,latestPer";
+     * MapTools.filterFields(list, mainFields, detailFields, subjectFields, financeFields, targetFields);
      * </pre>
-     * 
+     *
      * @param list 过滤前的数据
      * @param fields 需要保留的字段列表
      * @return 过滤后的数据
@@ -315,15 +690,15 @@ public class MapTools {
     /**
      * 过滤数据字段<br>
      * <pre>
-        Map&lt;String, Object&gt; data = { main:{}, detail:{}, subject:{}, finance:[], target:[] };
-        String mainFields = "main:id,projectName,publishStatus";
-        String detailFields = "detail:id,totalAmount,holdingTime";
-        String subjectFields = "subject:id,customerName,industryCategory";
-        String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
-        String targetFields = "target:id,industryCategory,latestMkt,latestPer";
-        MapTools.filterFields(data, mainFields, detailFields, subjectFields, financeFields, targetFields);
+     * Map&lt;String, Object&gt; data = { main:{}, detail:{}, subject:{}, finance:[], target:[] };
+     * String mainFields = "main:id,projectName,publishStatus";
+     * String detailFields = "detail:id,totalAmount,holdingTime";
+     * String subjectFields = "subject:id,customerName,industryCategory";
+     * String financeFields = "finance:id,totalAssets,netAssets,netProfits,busiIncome";
+     * String targetFields = "target:id,industryCategory,latestMkt,latestPer";
+     * MapTools.filterFields(data, mainFields, detailFields, subjectFields, financeFields, targetFields);
      * </pre>
-     * 
+     *
      * @param data 过滤前的数据
      * @param fields 需要保留的字段列表
      * @return 过滤后的数据
@@ -332,7 +707,7 @@ public class MapTools {
         if (VerifyTools.isAnyBlank(data, fields)) {
             return data;
         }
-        Map<String, Set<String>> conditions = parseConditioin(fields);
+        Map<String, Set<String>> conditions = parseCondition(fields);
         DepthMap result = new DepthMap();
         for (Map.Entry<String, Set<String>> entry : conditions.entrySet()) {
             String group = entry.getKey();
@@ -397,7 +772,7 @@ public class MapTools {
         return list;
     }
 
-    private static Map<String, Set<String>> parseConditioin(String... fields) {
+    private static Map<String, Set<String>> parseCondition(String... fields) {
         Map<String, Set<String>> conditions = new HashMap<>();
         for (String item : fields) {
             String[] array = StringTools.split(item, ';');
@@ -433,7 +808,8 @@ public class MapTools {
     }
 
     /** 树节点排序, 从顶级节点开始向下排列 **/
-    public static List<Map<String, Object>> sortTreeNodes(List<Map<String, Object>> list, String codeField, String parentField) {
+    public static List<Map<String, Object>> sortTreeNodes(List<Map<String, Object>> list, String codeField,
+            String parentField) {
         // key=codeValue
         Map<Object, Map<String, Object>> idMaps = new HashMap<>();
         // key=codeValue, value=children
@@ -480,4 +856,263 @@ public class MapTools {
         }
         return sorted;
     }
+
+    protected static boolean isComplexKey(String key) {
+        return key.indexOf('.') > 0 || key.indexOf('|') > 0 || key.indexOf('[') > 0 && key.indexOf(']') > 0;
+    }
+
+    /**
+     * 深度遍历Map内容 (注意: 此方法会改变map的内容, 如对象变成Map,数组变成List)
+     *
+     * @param map 根Map对象
+     * @param interceptor 拦截处理器
+     */
+    public static void each(Map<String, Object> map, EachInterceptor interceptor) {
+        doEachMap(null, map, interceptor);
+    }
+
+    /**
+     * 深层遍历Map内容 (注意: 此方法会改变map的内容, 如对象变成Map,数组变成List)
+     *
+     * @param list 根Map对象数组
+     * @param interceptor 拦截处理器
+     */
+    @SuppressWarnings("all")
+    public static void each(List<Map<String, Object>> list, EachInterceptor interceptor) {
+        // new ListEntry(list) jdk7语法检查报错
+        List<Object> objects = new ArrayList<>();
+        objects.addAll(list);
+        ListEntry listEntry = new ListEntry(objects);
+        for (int i = 0; i < list.size(); i++) {
+            String childName = "[" + i + "]";
+            Object childValue = list.get(i);
+            listEntry.init(childName, childName, i, childValue);
+            interceptor.intercept(listEntry);
+            if (listEntry.enter && listEntry.fieldValue != null) {
+                doEachHandleEntry(listEntry, interceptor);
+            }
+        }
+    }
+
+    private static void doEachMap(String parentPath, Map<String, Object> map, EachInterceptor interceptor) {
+        MapEntry mapEntry = new MapEntry(map);
+        for (Map.Entry<String, Object> item : map.entrySet()) {
+            if (item.getKey() == null || item.getKey().trim().length() == 0) {
+                continue;
+            }
+            String fieldName = item.getKey();
+            String fieldPath = StringTools.concat('.', parentPath, fieldName);
+            Object fieldValue = item.getValue();
+            mapEntry.init(fieldPath, fieldName, fieldValue);
+            interceptor.intercept(mapEntry);
+            if (mapEntry.enter && mapEntry.fieldValue != null) {
+                doEachHandleEntry(mapEntry, interceptor);
+            }
+        }
+    }
+
+    private static void doEachHandleEntry(FieldEntry mapEntry, EachInterceptor interceptor) {
+        String fieldPath = mapEntry.getFieldPath();
+        String fieldName = mapEntry.getFieldName();
+        Object fieldValue = mapEntry.getFieldValue();
+        List<Object> list = ConvertTools.parseList(fieldValue);
+        if (list.size() == 1 && list.get(0) == fieldValue) {
+            // 不是数组
+            if (fieldValue instanceof Map) {
+                Map<String, Object> child = MapTools.toJsonMap((Map<?, ?>) fieldValue);
+                doEachMap(fieldPath, child, interceptor);
+            } else if (!ReflectTools.isPrimitive(fieldValue.getClass(), false)) {
+                Map<String, Object> child = JsonTools.beanToMap(fieldValue, true, false);
+                mapEntry.setValue(child);
+                doEachMap(fieldPath, child, interceptor);
+            }
+        } else {
+            mapEntry.setValue(list);
+            ListEntry listEntry = new ListEntry(list);
+            for (int i = 0; i < list.size(); i++) {
+                String suffix = "[" + i + "]";
+                String childName = fieldName + suffix;
+                String childPath = fieldPath + suffix;
+                Object childValue = list.get(i);
+                listEntry.init(childPath, childName, i, childValue);
+                interceptor.intercept(listEntry);
+                if (listEntry.enter && listEntry.fieldValue != null) {
+                    doEachHandleEntry(listEntry, interceptor);
+                }
+            }
+        }
+    }
+
+    /** 遍历Map的拦截处理器 **/
+    public interface EachInterceptor {
+
+        /**
+         * 拦截处理方法
+         *
+         * @param entry 当前值
+         */
+        void intercept(FieldEntry entry);
+    }
+
+    /** 遍历Map的字段项变量 **/
+    public interface FieldEntry {
+
+        String getFieldPath();
+
+        String getFieldName();
+
+        Object getFieldValue();
+
+        void setValue(Object value);
+
+        /** 设置额外的参数 (如果父对象是数组, 则该操作无效) **/
+        void putExtraValue(String key, Object value);
+
+        /** 获取额外的参数 **/
+        MapPlus getExtraValues();
+
+        /** 是否已处理 (只要调用了setValue/putExtraValue就是已处理) **/
+        boolean isHandled();
+
+        /** 是否进入容器, 如果设置为false则不继续处理容器内容 (只在容器值时有效) **/
+        void setEnter(boolean enter);
+    }
+
+    private static class MapEntry implements FieldEntry {
+
+        private final Map<String, Object> map;
+        private String fieldPath;
+        private String fieldName;
+        private Object fieldValue;
+        private final MapPlus extras = new MapPlus();
+        /** 是否已处理 (只要调用了setValue/putExtraValue就是已处理) **/
+        private boolean handled;
+        /** 是否进入容器, 如果设置为false则不继续处理容器内容 (只在容器值时有效) **/
+        private boolean enter;
+
+        private MapEntry(Map<String, Object> map) {
+            this.map = map;
+        }
+
+        public void init(String fieldPath, String fieldName, Object fieldValue) {
+            this.fieldPath = fieldPath;
+            this.fieldName = fieldName;
+            this.fieldValue = fieldValue;
+            this.extras.clear();
+            this.handled = false;
+            this.enter = true;
+        }
+
+        @Override
+        public String getFieldPath() {
+            return fieldPath;
+        }
+
+        @Override
+        public String getFieldName() {
+            return fieldName;
+        }
+
+        @Override
+        public Object getFieldValue() {
+            return fieldValue;
+        }
+
+        @Override
+        public void setValue(Object value) {
+            this.map.put(this.fieldName, value);
+            this.fieldValue = value;
+            this.handled = true;
+        }
+
+        @Override
+        public void putExtraValue(String key, Object value) {
+            this.map.put(key, value);
+            this.extras.put(key, value);
+            this.handled = true;
+        }
+
+        @Override
+        public MapPlus getExtraValues() {
+            return new MapPlus(this.extras);
+        }
+
+        @Override
+        public boolean isHandled() {
+            return this.handled;
+        }
+
+        @Override
+        public void setEnter(boolean enter) {
+            this.enter = enter;
+        }
+    }
+
+    private static class ListEntry implements FieldEntry {
+
+        private final List<Object> list;
+        private String fieldPath;
+        private String fieldName;
+        private int index;
+        private Object fieldValue;
+        /** 是否已处理 (只要调用了setValue/putExtraValue就是已处理) **/
+        private boolean handled;
+        /** 是否进入容器, 如果设置为false则不继续处理容器内容 (只在容器值时有效) **/
+        private boolean enter;
+
+        private ListEntry(List<Object> list) {
+            this.list = list;
+        }
+
+        public void init(String fieldPath, String fieldName, int index, Object fieldValue) {
+            this.fieldPath = fieldPath;
+            this.fieldName = fieldName;
+            this.index = index;
+            this.fieldValue = fieldValue;
+            this.handled = false;
+            this.enter = true;
+        }
+
+        @Override
+        public String getFieldPath() {
+            return fieldPath;
+        }
+
+        @Override
+        public String getFieldName() {
+            return fieldName;
+        }
+
+        @Override
+        public Object getFieldValue() {
+            return fieldValue;
+        }
+
+        @Override
+        public void setValue(Object value) {
+            this.list.set(this.index, value);
+            this.fieldValue = value;
+            this.handled = true;
+        }
+
+        @Override
+        public void putExtraValue(String key, Object value) {
+            // 父对象是数组, 则该操作无效
+        }
+
+        @Override
+        public MapPlus getExtraValues() {
+            return null;
+        }
+
+        @Override
+        public boolean isHandled() {
+            return this.handled;
+        }
+
+        @Override
+        public void setEnter(boolean enter) {
+            this.enter = enter;
+        }
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/NamingTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/NamingTools.java
index e9fb3eaa4275aebc1d3846ce4d80080fc069dccc..ee4aa1a77cec92a5fa24f0fb82529acd5ad355e8 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/NamingTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/NamingTools.java
@@ -141,10 +141,10 @@ public class NamingTools {
      * A {@code null} input String returns {@code null}.
      *
      * <pre>
-     * StringTools.capitalize(null)  = null
-     * StringTools.capitalize("")    = ""
-     * StringTools.capitalize("cat") = "Cat"
-     * StringTools.capitalize("cAt") = "CAt"
+     * NamingTools.capitalize(null)  = null
+     * NamingTools.capitalize("")    = ""
+     * NamingTools.capitalize("cat") = "Cat"
+     * NamingTools.capitalize("cAt") = "CAt"
      * </pre>
      *
      * @param str the String to capitalize, may be null
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/NetworkTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/NetworkTools.java
index 6d843d3f7c507ca9f104fd3bf59fa1bceeccd33c..891944579ae53fc3b262d964d89c1478e5fd6973 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/NetworkTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/NetworkTools.java
@@ -11,7 +11,7 @@ import java.util.List;
 
 /**
  * 网络工具类
- * 
+ *
  * @author zhaohuihua
  * @version 190908
  */
@@ -43,7 +43,7 @@ public class NetworkTools {
 
     /**
      * 获取本机IPv4地址
-     * 
+     *
      * @return 本机IP地址
      * @deprecated use {@link #getLocalIpv4Addresses()}
      */
@@ -68,6 +68,13 @@ public class NetworkTools {
             if (network.isVirtual()) {
                 continue; // 忽略虚拟机的IP地址 (JDK8并没有作用)
             }
+            try {
+                if (network.isLoopback() || !network.isUp()) {
+                    continue;
+                }
+            } catch (SocketException ignore) {
+                continue;
+            }
             String name = network.getDisplayName().toLowerCase();
             if (name.contains("virtual") || name.contains("veth") || name.contains("virbr")
                     || name.contains("docker")) {
@@ -83,6 +90,9 @@ public class NetworkTools {
                 if (address.isLoopbackAddress() || address.getHostAddress().startsWith("127")) {
                     continue; // 忽略127.0.0.1地址, 127.xxx.xxx.xxx属于loopback, 只有本机可见
                 }
+                // if (!address.isReachable(50)) {
+                //     continue;
+                // }
                 if (address.isSiteLocalAddress()) { // 优先返回该地址
                     // 192.168.xxx.xxx属于private私有地址(SiteLocalAddress), 只能在本地局域网可见
                     // 公网地址一般都不会出现在网卡配置中, 都是通过映射访问的
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
index deba9f63e2d25d1caa1710ed2365bbb383fba58c..5e01b8a3cd577e86289ba68e841f4d0e8731eb6e 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RandomTools.java
@@ -98,14 +98,16 @@ public final class RandomTools {
      * @return 随机数
      */
     public static String generateNumber(final int length) {
-        StringBuilder buffer = new StringBuilder();
-        if (length > 0) {
-            // 确保第1位不是0
-            buffer.append(generateNumber(1, 9));
+        if (length <= 0) {
+            return "";
         }
-        if (length > 1) {
-            buffer.append(generateString(NUMBER_SOURCE, length - 1));
+        if (length <= 18) {
+            return String.valueOf(generateLongNumber(length));
         }
+        StringBuilder buffer = new StringBuilder();
+        // 确保第1位不是0
+        buffer.append(generateNumber(1, 9));
+        buffer.append(generateString(NUMBER_SOURCE, length - 1));
         return buffer.toString();
     }
 
@@ -141,7 +143,39 @@ public final class RandomTools {
      * @return 随机数
      */
     public static long generateNumber(final long min, final long max) {
-        return min + RANDOM.nextLong() % (max - min + 1);
+        return min + Math.abs(RANDOM.nextLong()) % (max - min + 1);
+    }
+
+    /**
+     * 生成指定长度的随机数
+     *
+     * @param length 随机数长度 (最大9位)
+     * @return 随机数
+     * @author zhaohuihua
+     */
+    public static int generateIntNumber(final int length) {
+        if (length >= 10) {
+            throw new IllegalArgumentException("length must be less than 10.");
+        }
+        int min = (int) Math.pow(10, length - 1);
+        int max = (int) Math.pow(10, length) - 1;
+        return generateNumber(min, max);
+    }
+
+    /**
+     * 生成指定长度的随机数
+     *
+     * @param length 随机数长度 (最大18位)
+     * @return 随机数
+     * @author zhaohuihua
+     */
+    public static long generateLongNumber(final int length) {
+        if (length >= 19) {
+            throw new IllegalArgumentException("length must be less than 19.");
+        }
+        long min = (long) Math.pow(10, length - 1);
+        long max = (long) Math.pow(10, length) - 1;
+        return generateNumber(min, max);
     }
 
     /**
@@ -150,6 +184,6 @@ public final class RandomTools {
      * @return 序列号
      */
     public static String generateUuid() {
-        return UUID.randomUUID().toString().replace("-", "").toUpperCase();
+        return StringTools.removeChars(UUID.randomUUID().toString(), '-').toUpperCase();
     }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
index 112a8344ae9d9e0430de52282f50e7fa7c7349d8..7e6dca723f1834e9b3ccb6200136b1b943d7048a 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/ReflectTools.java
@@ -15,6 +15,8 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import com.gitee.qdbp.able.beans.KeyValue;
+import com.gitee.qdbp.able.model.reusable.ExpressionMap;
 import com.gitee.qdbp.tools.parse.StringParser;
 
 /**
@@ -32,20 +34,20 @@ public abstract class ReflectTools {
     /**
      * 从对象中获取字段值, 支持多级字段名<br>
      * 如 target = { domain:{ text:"baidu", url:"http://baidu.com" } }<br>
-     * -- findFieldValue(target, "domain.text"); 返回 "baidu"<br>
+     * -- getDepthValue(target, "domain.text"); 返回 "baidu"<br>
      * 如 target = { domain:{ text:"baidu", url:"http://baidu.com" } }<br>
-     * -- findFieldValue(target, "domain.name || domain.text"); 返回 "baidu"<br>
+     * -- getDepthValue(target, "domain.name || domain.text"); 返回 "baidu"<br>
      * 如 list = [{ domain:{ text:"baidu", url:"http://baidu.com" } },<br>
      * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"bing", url:"http://cn.bing.com" } }]<br>
-     * -- findFieldValue(list, "[1].domain.text"); 返回 "bing"<br>
+     * -- getDepthValue(list, "[1].domain.text"); 返回 "bing"<br>
      * 如 data = { data: [<br>
      * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"baidu", url:"http://baidu.com" } },<br>
      * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"bing", url:"http://cn.bing.com" } }<br>
      * ] }<br>
-     * -- findFieldValue(list, "data[1].domain.text"); 返回 "bing"<br>
+     * -- getDepthValue(list, "data[1].domain.text"); 返回 "bing"<br>
      *
      * @param target 目标对象, 支持数组/List/Map/Bean
-     * @param fieldNames 字段名, 支持带点的多级字段名和数组下标
+     * @param fieldNames 字段名, 支持带点的多级字段名/数组下标/负数下标
      * @return 字段值
      */
     @SuppressWarnings("unchecked")
@@ -66,34 +68,38 @@ public abstract class ReflectTools {
 
     private static Object doGetDepthValue(Object target, String fields) {
         // 将表达式以.或[]拆分为数组
-        String[] list = StringParser.splitFields(fields);
+        List<String> fieldNames = StringParser.splitsFields(fields);
+        // 清除空值和this
+        clearFieldNames(fieldNames);
         // 逐层取值
         Object value = target;
-        for (int j = 0, z = list == null ? 0 : list.length; value != null && j < z; j++) {
-            String field = list[j];
-            if (VerifyTools.isNotBlank(field) && !field.equals("this")) {
-                if (value instanceof List) {
-                    value = getListFieldValue((List<?>) value, field);
-                } else if (value instanceof Map) {
-                    value = getMapFieldValue((Map<?, ?>) value, field);
-                } else if (value.getClass().isArray()) {
-                    value = getArrayFieldValue(value, field);
-                } else if (value instanceof Iterable) {
-                    value = getIterableFieldValue((Iterable<?>) value, field);
-                } else if (value instanceof Iterator) {
-                    value = getIteratorFieldValue((Iterator<?>) value, field);
-                } else if (value instanceof Enumeration) {
-                    value = getEnumerationFieldValue((Enumeration<?>) value, field);
-                } else {
-                    value = getFieldValue(value, field, false);
-                }
+        for (int i = 0, z = fieldNames.size(); value != null && i < z; i++) {
+            String field = fieldNames.get(i);
+            if (value instanceof List) {
+                value = getListFieldValue((List<?>) value, field);
+            } else if (value instanceof Map) {
+                value = getMapFieldValue((Map<?, ?>) value, field);
+            } else if (value.getClass().isArray()) {
+                value = getArrayFieldValue(value, field);
+            } else if (value instanceof Iterable) {
+                value = getIterableFieldValue((Iterable<?>) value, field);
+            } else if (value instanceof Iterator) {
+                value = getIteratorFieldValue((Iterator<?>) value, field);
+            } else if (value instanceof Enumeration) {
+                value = getEnumerationFieldValue((Enumeration<?>) value, field);
+            } else {
+                value = getFieldValue(value, field, false);
             }
         }
         return value;
     }
 
     private static Object getMapFieldValue(Map<?, ?> map, String fieldName) {
-        return map.get(fieldName);
+        if (map instanceof ExpressionMap) {
+            return ((ExpressionMap) map).getOriginal(fieldName);
+        } else {
+            return map.get(fieldName);
+        }
     }
 
     private static Object getArrayFieldValue(Object array, String fieldName) {
@@ -218,6 +224,195 @@ public abstract class ReflectTools {
         }
     }
 
+    /**
+     * 从对象中获取字段值, 支持多级字段名<br>
+     * 如 target = { domain:{ text:"baidu", url:"http://baidu.com" } }<br>
+     * -- findDepthValues(target, "domain.text"); 返回 [{ key:"domain.text", value:"baidu" }]<br>
+     * 如 target = { domain:{ text:"baidu", url:"http://baidu.com" } }<br>
+     * -- findDepthValues(target, "domain.name || domain.text"); 返回 [{ key:"domain.text", value:"baidu" }]<br>
+     * 如 list = [{ domain:{ text:"baidu", url:"http://baidu.com" } },<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"bing", url:"http://cn.bing.com" } }]<br>
+     * -- findDepthValues(list, "domain.text");<br>
+     * -- 返回 [{ key:"[0].domain.text", value:"baidu" },{ key:"[1].domain.text", value:"bing" }]<br>
+     * 如 data = { data: [<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"baidu", url:"http://baidu.com" } },<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{ text:"bing", url:"http://cn.bing.com" } }<br>
+     * ] }<br>
+     * -- findDepthValues(list, "data.domain.text");<br>
+     * -- 返回 [{ key:"data[0].domain.text", value:"baidu" },{ key:"data[1].domain.text", value:"bing" }]<br>
+     * 如 data = { data: [<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:{} },<br>
+     * &nbsp;&nbsp;&nbsp;&nbsp;{ domain:null }<br>
+     * ] }<br>
+     * -- findDepthValues(list, "data.domain.text");<br>
+     * -- 返回 [{ key:"data[0].domain.text", value:null }] // 上级为空的, 不返回 (第2个domain为空)<br>
+     * 如 data = { data: [] }<br>
+     * -- findDepthValues(list, "data.domain.text"); 返回 []<br>
+     *
+     * @param target 目标对象, 支持数组/List/Map/Bean
+     * @param fieldNames 字段名, 支持带点的多级字段名/数组下标/负数下标
+     * @return 字段值
+     */
+    public static List<KeyValue<Object>> findDepthValues(Object target, String fieldNames) {
+        VerifyTools.requireNotBlank(fieldNames, "field");
+        if (target == null) {
+            return new ArrayList<>();
+        }
+        String[] orFields = StringTools.split(fieldNames, "||");
+        for (int i = 0, z = orFields.length - 1; i <= z; i++) {
+            String fields = orFields[i];
+            List<KeyValue<Object>> result = doFindDepthValues(target, fields);
+            if (i == z) {
+                // 最后一个OR字段, 直接返回
+                return result;
+            } else {
+                // 不是最后一个OR字段, 判断是否存在非空的值
+                boolean existValue = false;
+                for (KeyValue<Object> item : result) {
+                    if (VerifyTools.isNotBlank(item.getValue())) {
+                        existValue = true;
+                    }
+                }
+                if (existValue) {
+                    return result;
+                }
+            }
+        }
+        return new ArrayList<>();
+    }
+
+    private static List<KeyValue<Object>> doFindDepthValues(Object target, String fields) {
+        // 将表达式以.或[]拆分为数组
+        List<String> fieldNames = StringParser.splitsFields(fields);
+        // 清除空值和this
+        clearFieldNames(fieldNames);
+        // 逐层取值
+        List<KeyValue<Object>> items = parseListValues(null, target, needExpandList(fieldNames, 0));
+        for (int i = 0, z = fieldNames.size() - 1; i <= z; i++) {
+            String field = fieldNames.get(i);
+            // 是否需要展开 (有下一层且下一层是数字时,不需要展开)
+            boolean expand = needExpandList(fieldNames, i + 1);
+            List<KeyValue<Object>> temp = new ArrayList<>();
+            for (KeyValue<Object> item : items) {
+                // 上级字段名
+                String parentFields = item.getKey();
+                // 本级字段值
+                Object currValue = item.getValue();
+                // 下级字段值
+                Object nextValue;
+                // 下级字段名
+                boolean currList;
+                if (currValue == null) {
+                    nextValue = null;
+                    currList = false;
+                } else if (currValue instanceof List) {
+                    nextValue = getListFieldValue((List<?>) currValue, field);
+                    currList = true;
+                } else if (currValue.getClass().isArray()) {
+                    nextValue = getArrayFieldValue(currValue, field);
+                    currList = true;
+                } else if (currValue instanceof Iterable) {
+                    nextValue = getIterableFieldValue((Iterable<?>) currValue, field);
+                    currList = true;
+                } else if (currValue instanceof Iterator) {
+                    nextValue = getIteratorFieldValue((Iterator<?>) currValue, field);
+                    currList = true;
+                } else if (currValue instanceof Enumeration) {
+                    nextValue = getEnumerationFieldValue((Enumeration<?>) currValue, field);
+                    currList = true;
+                } else if (currValue instanceof Map) {
+                    nextValue = getMapFieldValue((Map<?, ?>) currValue, field);
+                    currList = false;
+                } else {
+                    nextValue = getFieldValue(currValue, field, false);
+                    currList = false;
+                }
+                String nextField;
+                if (currList) {
+                    nextField = StringTools.concat(parentFields, "[" + field + "]");
+                } else {
+                    nextField = StringTools.concat('.', parentFields, field);
+                }
+                // 最末级即使值为空也要加入结果集, 不是最末级则只要值为空就不再继续
+                boolean isLastField = i >= z;
+                if (isLastField || nextValue != null) {
+                    temp.addAll(parseListValues(nextField, nextValue, expand));
+                }
+            }
+            items = temp;
+        }
+        return items;
+    }
+
+    @SuppressWarnings("all")
+    private static void clearFieldNames(List<String> fieldNames) {
+        // 清除空值和this
+        Iterator<String> iterator = fieldNames.iterator();
+        while (iterator.hasNext()) {
+            String next = iterator.next();
+            if (VerifyTools.isBlank(next) || next.equals("this")) {
+                iterator.remove();
+            }
+        }
+    }
+
+    // 根据字段名判断是否需要展开 (有下一层且下一层是数字时,说明是取数组的其中一项,不需要展开)
+    private static boolean needExpandList(List<String> fields, int index) {
+        if (index >= fields.size()) {
+            return true;
+        } else {
+            String field = fields.get(index);
+            return !StringTools.isNumber(field) || field.indexOf('.') >= 0;
+        }
+    }
+
+    private static List<KeyValue<Object>> parseListValues(String field, Object value, boolean expand) {
+        List<KeyValue<Object>> results = new ArrayList<>();
+        if (!expand) {
+            results.add(new KeyValue<>(field, value));
+        } else {
+            if (value == null) {
+                results.add(new KeyValue<>(field, null));
+            } else if (value instanceof List) {
+                List<?> values = (List<?>) value;
+                for (int i = 0; i < values.size(); i++) {
+                    String key = StringTools.concat(field, "[" + i + "]");
+                    results.add(new KeyValue<>(key, values.get(i)));
+                }
+            } else if (value.getClass().isArray()) {
+                Object[] values = (Object[]) value;
+                for (int i = 0; i < values.length; i++) {
+                    String key = StringTools.concat(field, "[" + i + "]");
+                    results.add(new KeyValue<>(key, values[i]));
+                }
+            } else if (value instanceof Iterable) {
+                Iterable<?> iterable = (Iterable<?>) value;
+                int i = 0;
+                for (Object item : iterable) {
+                    String key = StringTools.concat(field, "[" + (i++) + "]");
+                    results.add(new KeyValue<>(key, item));
+                }
+            } else if (value instanceof Iterator) {
+                Iterator<?> iterator = (Iterator<?>) value;
+                int i = 0;
+                while (iterator.hasNext()) {
+                    String key = StringTools.concat(field, "[" + (i++) + "]");
+                    results.add(new KeyValue<>(key, iterator.next()));
+                }
+            } else if (value instanceof Enumeration) {
+                Enumeration<?> enumeration = (Enumeration<?>) value;
+                int i = 0;
+                while (enumeration.hasMoreElements()) {
+                    String key = StringTools.concat(field, "[" + (i++) + "]");
+                    results.add(new KeyValue<>(key, enumeration.nextElement()));
+                }
+            } else {
+                results.add(new KeyValue<>(field, value));
+            }
+        }
+        return results;
+    }
+
     /**
      * 深度判断对象中是否存在指定的字段, 支持多级字段名
      *
@@ -1129,6 +1324,241 @@ public abstract class ReflectTools {
         }
     }
 
+    /**
+     * 查找无参数的构造函数
+     *
+     * @param clazz 类
+     * @return 构造函数对象, 如果未找到则返回null
+     */
+    public static <T> Constructor<T> findConstructor(Class<T> clazz) {
+        return findConstructor(clazz, false, new Class<?>[0]);
+    }
+
+    /**
+     * 查找无参数的构造函数
+     *
+     * @param clazz 类
+     * @param throwOnNotFound 如果构造函数不存在是否抛出异常
+     * @return 构造函数对象
+     */
+    public static <T> Constructor<T> findConstructor(Class<T> clazz, boolean throwOnNotFound) {
+        return findConstructor(clazz, throwOnNotFound, new Class<?>[0]);
+    }
+
+    /**
+     * 优先选择参数类型完全相同的, 其次选择通过装箱/拆箱能匹配的或父子关系能匹配的<br>
+     * 总之:<br>
+     * 如果有一个构造函数Constructor(List&lt;?&gt; list, Integer i)<br>
+     * 通过eachFindConstructor(clazz, List.class, Integer.class);能找到<br>
+     * 通过eachFindConstructor(clazz, List.class, int.class);能找到<br>
+     * 通过eachFindConstructor(clazz, ArrayList.class, Integer.class);能找到<br>
+     * 通过eachFindConstructor(clazz, ArrayList.class, int.class);也能找到<br>
+     *
+     * @param clazz 类
+     * @param types 参数列表
+     * @return 构造函数对象, 如果未找到则返回null
+     */
+    public static <T> Constructor<T> findConstructor(Class<T> clazz, Class<?>... types) {
+        return findConstructor(clazz, false, types);
+    }
+
+    /**
+     * 优先选择参数类型完全相同的, 其次选择通过装箱/拆箱能匹配的或父子关系能匹配的<br>
+     * 总之:<br>
+     * 如果有一个构造函数Constructor(List&lt;?&gt; list, Integer i)<br>
+     * 通过eachFindConstructor(clazz, List.class, Integer.class);能找到<br>
+     * 通过eachFindConstructor(clazz, List.class, int.class);能找到<br>
+     * 通过eachFindConstructor(clazz, ArrayList.class, Integer.class);能找到<br>
+     * 通过eachFindConstructor(clazz, ArrayList.class, int.class);也能找到<br>
+     *
+     * @param clazz 类
+     * @param throwOnNotFound 如果构造函数不存在是否抛出异常
+     * @param types 参数列表, 如果其中某个参数为null, 则不检查类型
+     * @return 构造函数对象
+     */
+    public static <T> Constructor<T> findConstructor(Class<T> clazz, boolean throwOnNotFound, Class<?>... types) {
+        VerifyTools.requireNonNull(clazz, "clazz");
+
+        try {
+            return clazz.getConstructor(types);
+        } catch (NoSuchMethodException e) {
+            if (types != null && types.length > 0) {
+                return eachFindConstructor(clazz, throwOnNotFound, types);
+            } else {
+                if (throwOnNotFound) {
+                    String signature = getMethodLogSignature(clazz, clazz.getSimpleName(), types);
+                    throw new IllegalArgumentException(signature + " not found.");
+                } else {
+                    return null;
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings({"unchecked"})
+    private static <T> Constructor<T> eachFindConstructor(Class<T> clazz, boolean throwOnNotFound, Class<?>... types) {
+        // 直接采用clazz.getConstructor(types)的方式
+        // 会出现根据int找不到clazzName(Integer), 根据Integer找不到clazzName(Object)的情况
+
+        List<Constructor<?>> accepted = new ArrayList<>(); // 可接受的(通过装箱/拆箱能匹配的或父子关系能匹配的)
+
+        int length = types.length;
+        Constructor<?>[] methods = clazz.getConstructors();
+
+        for (Constructor<?> m : methods) {
+            Class<?>[] actuals = m.getParameterTypes();
+            if (actuals.length != types.length) {
+                continue; // 参数个数不符
+            }
+            boolean equals = true;
+            boolean accept = true;
+            for (int i = 0; i < length; i++) {
+                Class<?> a = actuals[i];
+                Class<?> t = types[i];
+                if (t == null) {
+                    equals = false;
+                    continue;
+                }
+                if (a != t) {
+                    equals = false;
+                }
+                if (a != t && !a.isAssignableFrom(t) && !isCompatible(a, t)) {
+                    accept = false;
+                }
+            }
+            if (equals) {
+                return (Constructor<T>) m; // 参数类型完全相同, 直接返回
+            }
+            if (accept) {
+                accepted.add(m);
+            }
+        }
+
+        if (accepted.isEmpty()) {
+            if (throwOnNotFound) {
+                String signature = getMethodLogSignature(clazz, clazz.getSimpleName(), types);
+                throw new IllegalArgumentException(signature + " not found.");
+            } else {
+                return null;
+            }
+        } else { // 没有参数类型完全相同, 但有通过装箱/拆箱能匹配的或父子关系能匹配的
+            if (accepted.size() == 1) { // 如果只有一个这种构造函数, 就选择这个啦
+                return (Constructor<T>) accepted.get(0);
+            } else { // 如果不只一个, 那没办法, 报个错吧
+                String signature = getMethodLogSignature(clazz, clazz.getSimpleName(), types);
+                throw new IllegalArgumentException(signature + " is ambiguous.");
+            }
+        }
+    }
+
+    /**
+     * 执行无参数的构造函数
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param constructor 构造函数
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Constructor<T> constructor) {
+        VerifyTools.requireNonNull(constructor, "constructor");
+        return newInstance(constructor, new Object[0]);
+    }
+
+    /**
+     * 创建新对象实例
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param constructor 构造函数
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Constructor<T> constructor, Object... args) {
+        VerifyTools.requireNonNull(constructor, "constructor");
+
+        try {
+            return constructor.newInstance(args);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw renewMethodRuntimeException(e);
+        }
+    }
+
+    /**
+     * 执行无参数的构造函数
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param clazz Class
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Class<T> clazz) {
+        return newInstance(clazz, true);
+    }
+
+    /**
+     * 执行无参数的构造函数
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param clazz Class
+     * @param throwOnMethodNotFound 如果默认构造函数不存在是否抛出异常
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Class<T> clazz, boolean throwOnMethodNotFound) {
+        VerifyTools.requireNotBlank(clazz, "clazz");
+
+        Class<?>[] types = new Class<?>[0];
+        Constructor<T> constructor = findConstructor(clazz, throwOnMethodNotFound, types);
+        if (constructor == null) {
+            return null;
+        }
+        return newInstance(constructor, new Object[0]);
+    }
+
+    /**
+     * 创建新对象实例<br>
+     * 如果根据构造函数名和参数类型找到多个构造函数将抛出IllegalArgumentException异常<br>
+     * 在args中如果包含null对象将不会判断参数类型, 如果有多个同名且参数个数相同的构造函数将有可能因无法确定目标构造函数而报错
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param clazz Class
+     * @param args 参数
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Class<T> clazz, Object... args) {
+        return newInstance(clazz, true, args);
+    }
+
+    /**
+     * 创建新对象实例<br>
+     * 如果根据构造函数名和参数类型找到多个构造函数将抛出IllegalArgumentException异常<br>
+     * 在args中如果包含null对象将不会判断参数类型, 如果有多个同名且参数个数相同的构造函数将有可能因无法确定目标构造函数而报错
+     *
+     * @param <T> 返回结果类型, 如果类型不符将抛出ClassCastException异常
+     * @param clazz Class
+     * @param throwOnMethodNotFound 如果构造函数不存在是否抛出异常
+     * @param args 参数
+     * @return 对象实例
+     */
+    public static <T> T newInstance(Class<T> clazz, boolean throwOnMethodNotFound, Object... args) {
+        VerifyTools.requireNotBlank(clazz, "clazz");
+
+        int size = args == null ? 0 : args.length;
+        Class<?>[] types = new Class<?>[size];
+        for (int i = 0; i < size; i++) {
+            types[i] = args[i] == null ? null : args[i].getClass();
+        }
+        Constructor<T> constructor = findConstructor(clazz, throwOnMethodNotFound, types);
+        if (constructor == null) {
+            return null;
+        }
+
+        try {
+            return constructor.newInstance(args);
+        } catch (RuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            throw renewMethodRuntimeException(e);
+        }
+    }
+
     /** 判断参数是否能够匹配 **/
     private static boolean isCompatible(Class<?> a, Class<?> b) {
         if (a == b) {
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9b734732d8570f64c12e9f79bab8bae6d30f617
--- /dev/null
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/RuleTools.java
@@ -0,0 +1,336 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.beans.KeyStrings;
+import com.gitee.qdbp.able.beans.StringWithAttrs;
+
+/**
+ * 规则配置解析工具类
+ *
+ * @author zhaohuihua
+ * @version 20220129
+ */
+public class RuleTools {
+
+    private RuleTools() {
+    }
+
+    /**
+     * 按换行拆分字符串, 并清除注释行
+     *
+     * @param string 原字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<String> splitRuleLines(String string) {
+        if (string == null) {
+            return null;
+        }
+        List<String> list = StringTools.splits(string, false, '\n');
+        return clearCommentLines(list);
+    }
+
+    /**
+     * 清除注释行
+     *
+     * @param strings 原字符串数组
+     * @return 清除后的字符串数组
+     */
+    public static List<String> clearCommentLines(List<String> strings) {
+        if (strings == null) {
+            return null;
+        }
+        List<String> list = new ArrayList<>();
+        for (String string : strings) {
+            if (string == null || string.length() == 0) {
+                continue;
+            }
+            String trimmed = string.trim();
+            if (trimmed.length() == 0 || trimmed.charAt(0) == '#' || trimmed.startsWith("//")) {
+                // 遇到注释行, 跳过
+                continue;
+            }
+            list.add(string);
+        }
+        return list;
+    }
+
+    /**
+     * 按指定的分隔符将字符串拆分为片段<br>
+     * 例如: 将一大段带换行的文本按######拆分成多个小段<pre>
+     * |######
+     * |aaa
+     * |bbb
+     * |######
+     * |ccc
+     * |######
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<String> splitRuleFragments(String rules) {
+        return splitRuleFragments(rules, '#');
+    }
+
+    /**
+     * 按指定的分隔符将字符串拆分为片段<br>
+     * 例如: 将一大段带换行的文本按######拆分成多个小段<pre>
+     * |######
+     * |aaa
+     * |bbb
+     * |######
+     * |ccc
+     * |######
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @param delimiter 分隔符
+     * @return 拆分后的字符串数组
+     */
+    public static List<String> splitRuleFragments(String rules, char delimiter) {
+        List<String> strings = StringTools.splits(rules, false, '\n');
+        List<String> results = new ArrayList<>();
+        List<String> buffer = new ArrayList<>();
+        for (int i = 0, z = strings.size() - 1; i <= z; i++) {
+            String string = strings.get(i);
+            if (isRuleLineDelimiter(string, delimiter)) {
+                // 遇到分隔符
+                results.add(ConvertTools.joinToString(buffer, '\n'));
+                buffer.clear();
+            } else {
+                buffer.add(string);
+            }
+        }
+        if (!buffer.isEmpty()) {
+            results.add(ConvertTools.joinToString(buffer, '\n'));
+        }
+        return results;
+    }
+
+    private static boolean isRuleLineDelimiter(String string, char delimiter) {
+        String t = string.trim();
+        int len = t.length();
+        int min = 6;
+        if (len > min && t.charAt(0) == delimiter && t.charAt(1) == delimiter && t.charAt(len - 1) == delimiter) {
+            if (StringTools.countCharacter(t, delimiter) >= min) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static Map<String, Object> splitRuleMaps(String rules) {
+        KeyStrings values = splitRuleKeyValues(rules);
+        Map<String, Object> map = new HashMap<>();
+        for (KeyString item : values) {
+            String key = item.getKey();
+            if (key == null) {
+                continue;
+            }
+            String value = item.getValue();
+            if (value == null) {
+                map.put(key, null);
+            } else {
+                map.put(key, ConvertTools.tryParsePrimitiveValue(key, value));
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 按指定的分隔符将字符串拆分为键值对, 且按缩进合并行 (缩进的行与其上一行合并)<br>
+     * 如下文本, 返回三项, 分别为 [ name, class, params ], 其中 params = Value1\nValue2\nValue3
+     * <pre>
+     * |name = 披露渠道检查
+     * |class = StringMatcherExecutor
+     * |params =
+     * |    Value1
+     * |    Value2
+     * |    Value3
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static KeyStrings splitRuleKeyValues(String rules) {
+        List<String> strings = StringTools.splits(rules, false, '\n');
+        return splitRuleKeyValues(strings);
+    }
+
+    /**
+     * 按指定的分隔符将字符串拆分为键值对, 且按缩进合并行 (缩进的行与其上一行合并)<br>
+     * 如下文本, 返回三项, 分别为 [ name, class, params ], 其中 params = Value1\nValue2\nValue3
+     * <pre>
+     * |name = 披露渠道检查
+     * |class = StringMatcherExecutor
+     * |params =
+     * |    Value1
+     * |    Value2
+     * |    Value3
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static KeyStrings splitRuleKeyValues(List<String> rules) {
+        KeyStrings results = new KeyStrings();
+        if (rules == null) {
+            return results;
+        }
+        for (int i = 0, z = rules.size(); i < z; i++) {
+            String string = rules.get(i);
+            String trimmed = string.trim();
+            if (trimmed.length() == 0) {
+                continue;
+            }
+            if (trimmed.charAt(0) == '#' || trimmed.startsWith("//")) {
+                // 遇到注释行, 跳过
+                continue;
+            }
+            int index = StringTools.charIndex(trimmed, '=', ':');
+            String key = null;
+            List<String> values = new ArrayList<>();
+            if (index < 0) {
+                values.add(trimmed);
+            } else {
+                if (index > 0) {
+                    key = StringTools.trimRight(trimmed.substring(0, index));
+                }
+                String value = StringTools.trimLeft(trimmed.substring(index + 1));
+                if (value.length() > 0) {
+                    values.add(value);
+                }
+            }
+            i += collectNextIndentLines(rules, i + 1, values);
+            results.add(new KeyString(key, ConvertTools.joinToString(values, '\n')));
+        }
+        return results;
+    }
+
+    /**
+     * 拆分一行文本多行属性的格式, 属性必须带缩进<br>
+     * 如下文本, 返回2项, 分别为<br>
+     * { test1, attrs:{ attr11:"value11", attr12:"value12" } }<br>
+     * { test2, attrs:{ attr21:"value21", attr22:"value22", attr23:"value23" } }
+     * <pre>
+     * |test1
+     * |    attr11 = value11
+     * |    attr12 = value12
+     * |test2
+     * |    attr21 = value21
+     * |    attr22 = value22
+     * |    attr23 = value23
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<StringWithAttrs> splitRuleWithAttrs(String rules) {
+        List<String> strings = StringTools.splits(rules, false, '\n');
+        return splitRuleWithAttrs(strings);
+    }
+
+    /**
+     * 拆分一行文本多行属性的格式, 属性必须带缩进<br>
+     * 如下文本, 返回2项, 分别为<br>
+     * { test1, attrs:{ attr11:"value11", attr12:"value12" } }<br>
+     * { test2, attrs:{ attr21:"value21", attr22:"value22", attr23:"value23" } }
+     * <pre>
+     * |test1
+     * |    attr11 = value11
+     * |    attr12 = value12
+     * |test2
+     * |    attr21 = value21
+     * |    attr22 = value22
+     * |    attr23 = value23
+     * </pre>
+     *
+     * @param rules 规则字符串
+     * @return 拆分后的字符串数组
+     */
+    public static List<StringWithAttrs> splitRuleWithAttrs(List<String> rules) {
+        List<StringWithAttrs> results = new ArrayList<>();
+        for (int i = 0, z = rules.size(); i < z; i++) {
+            String string = rules.get(i);
+            String trimmed = string.trim();
+            if (trimmed.length() == 0) {
+                continue;
+            }
+            if (trimmed.charAt(0) == '#' || trimmed.startsWith("//")) {
+                // 遇到注释行, 跳过
+                continue;
+            }
+            KeyStrings attrs = new KeyStrings();
+            i += collectNextIndentAttrs(rules, i + 1, attrs);
+            results.add(new StringWithAttrs(trimmed, attrs));
+        }
+        return results;
+    }
+
+    public static int collectNextIndentAttrs(List<String> rules, int start, List<KeyString> collectors) {
+        List<String> lines = new ArrayList<>();
+        int lineIndex = collectNextIndentLines(rules, start, lines);
+        for (String line : lines) {
+            String trimmed = line.trim();
+            int index = StringTools.charIndex(trimmed, '=', ':');
+            String key = null;
+            String value = trimmed;
+            if (index > 0) {
+                key = StringTools.trimRight(trimmed.substring(0, index));
+            }
+            if (index >= 0) {
+                value = StringTools.trimLeft(trimmed.substring(index + 1));
+            }
+            collectors.add(new KeyString(key, value));
+        }
+        return lineIndex;
+    }
+
+    public static int collectNextIndentLines(List<String> rules, int start, List<String> collectors) {
+        int count = 0;
+        int firstLeadingSpaceSize = -1;
+        for (int i = start, z = rules.size(); i < z; i++, count++) {
+            String next = rules.get(i);
+            String trimmed = next.trim();
+            if (trimmed.length() == 0) {
+                continue;
+            }
+            if (trimmed.charAt(0) == '#' || trimmed.startsWith("//")) {
+                // 遇到注释行, 跳过
+                continue;
+            }
+            // 不是缩进, 但是以右括号开头, 也与上一行合并
+            char first = next.charAt(0);
+            if (first == '}' || first == ']' || first == ')') {
+                collectors.add(next);
+                // 遇到未缩进的行, 说明已经结束了
+                break;
+            }
+            // 计算缩进量
+            int leadingSpaceSize = IndentTools.countLeadingSpaceSize(next);
+            if (leadingSpaceSize < 2) {
+                // 遇到未缩进的行, 说明已经结束了
+                break;
+            }
+            if (firstLeadingSpaceSize < 0) {
+                // 遇到第1个有效行, 记录缩进数量
+                firstLeadingSpaceSize = leadingSpaceSize;
+                collectors.add(StringTools.trimLeft(next));
+            } else if (leadingSpaceSize <= firstLeadingSpaceSize) {
+                // 比第1行缩进还少, 只能清除左侧缩进, 与第1行对齐
+                collectors.add(StringTools.trimLeft(next));
+            } else {
+                // 比第1行缩进更多的, 保留多出部分的缩进空格
+                char[] indentSpaces = new char[leadingSpaceSize - firstLeadingSpaceSize];
+                Arrays.fill(indentSpaces, ' ');
+                collectors.add(new String(indentSpaces) + StringTools.trimLeft(next));
+            }
+        }
+        return count;
+    }
+}
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
index e6f57ca8cdf083e68fe77e28d816d0addb301b74..0a59f3d3603a14f6c0ec947103a20f53b591a822 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/StringTools.java
@@ -70,7 +70,6 @@ public class StringTools {
     /**
      * 判断字符串是不是手机号码
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是手机号码, 如果字符串等于null或空字符串, 返回false
      */
@@ -84,7 +83,6 @@ public class StringTools {
     /**
      * 判断字符串是不是手机号码
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是手机号码, 如果字符串等于null或空字符串, 返回false
      */
@@ -98,7 +96,6 @@ public class StringTools {
     /**
      * 判断字符串是不是邮箱地址
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是邮箱地址, 如果字符串等于null或空字符串, 返回false
      */
@@ -112,7 +109,6 @@ public class StringTools {
     /**
      * 判断字符串是不是邮箱地址
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是邮箱地址, 如果字符串等于null或空字符串, 返回false
      */
@@ -126,7 +122,6 @@ public class StringTools {
     /**
      * 判断字符串是不是网址
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是网址, 如果字符串等于null或空字符串, 返回false
      */
@@ -141,7 +136,6 @@ public class StringTools {
     /**
      * 判断字符串是不是网址
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是网址, 如果字符串等于null或空字符串, 返回false
      */
@@ -155,7 +149,6 @@ public class StringTools {
     /**
      * 判断字符串是不是纯数字
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是数字, 如果字符串等于null或空字符串, 返回false
      */
@@ -175,7 +168,6 @@ public class StringTools {
     /**
      * 判断字符串是不是纯数字
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是数字, 如果字符串等于null或空字符串, 返回false
      */
@@ -189,7 +181,6 @@ public class StringTools {
     /**
      * 判断字符串是不是数字 (可带+-号/小数点/逗号)
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是数字, 如果字符串等于null或空字符串, 返回false
      */
@@ -251,7 +242,6 @@ public class StringTools {
     /**
      * 判断字符串是不是数字 (可带+-号/小数点/逗号)
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是数字, 如果字符串等于null或空字符串, 返回false
      */
@@ -265,7 +255,6 @@ public class StringTools {
     /**
      * 判断字符串是不是英文字符
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是英文字符, 如果字符串等于null或空字符串, 返回false
      */
@@ -285,7 +274,6 @@ public class StringTools {
     /**
      * 判断字符串是不是英文字符
      *
-     * @author zhaohuihua
      * @param string 字符串
      * @return 是不是英文字符, 如果字符串等于null或空字符串, 返回false
      */
@@ -298,7 +286,7 @@ public class StringTools {
 
     /**
      * 是不是单词字符
-     * 
+     *
      * @param c 字符
      * @return 是不是单词字符
      */
@@ -306,9 +294,30 @@ public class StringTools {
         return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_' || c == '$';
     }
 
+    /**
+     * 是不是单词字符
+     *
+     * @param c 字符
+     * @param allowChars 除字母数字以外的允许字符
+     * @return 是不是单词字符
+     */
+    private static boolean isWordChar(char c, char... allowChars) {
+        if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9') {
+            return true;
+        }
+        if (allowChars != null) {
+            for (int i = 0; i < allowChars.length; i++) {
+                if (c == allowChars[i]) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * 是不是单词
-     * 
+     *
      * @param string 字符串
      * @return 是不是单词
      */
@@ -316,30 +325,64 @@ public class StringTools {
         if (string == null || string.length() == 0) {
             return false;
         }
-        char[] chars = string.toCharArray();
-        char fc = chars[0];
-        if (fc >= '0' && fc <= '9') {
-            return false; // 以数字开头的, 不是单词
+        char[] allowChars = { '_', '$' };
+        return checkWordString(string, allowChars);
+    }
+
+    /**
+     * 是不是单词
+     *
+     * @param string 字符串
+     * @return 是不是单词
+     */
+    public static boolean isWordString(CharSequence string) {
+        if (string == null || string.length() == 0) {
+            return false;
         }
-        for (int i = 0; i < chars.length; i++) {
-            if (!isWordChar(chars[i])) {
-                return false;
-            }
+        char[] allowChars = { '_', '$' };
+        return checkWordString(string.toString(), allowChars);
+    }
+
+    /**
+     * 是不是单词
+     *
+     * @param string 字符串
+     * @param allowChars 除字母数字以外的允许字符
+     * @return 是不是单词
+     */
+    public static boolean isWordString(String string, char... allowChars) {
+        if (string == null || string.length() == 0) {
+            return false;
         }
-        return true;
+        return checkWordString(string, allowChars);
     }
 
     /**
      * 是不是单词
-     * 
+     *
      * @param string 字符串
+     * @param allowChars 除字母数字以外的允许字符
      * @return 是不是单词
      */
-    public static boolean isWordString(CharSequence string) {
+    public static boolean isWordString(CharSequence string, char... allowChars) {
         if (string == null || string.length() == 0) {
             return false;
         }
-        return isWordString(string.toString());
+        return checkWordString(string.toString(), allowChars);
+    }
+
+    private static boolean checkWordString(String string, char... allowChars) {
+        char fc = string.charAt(0);
+        if (fc >= '0' && fc <= '9') {
+            return false; // 以数字开头的, 不是单词
+        }
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (!isWordChar(c, allowChars)) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
@@ -353,7 +396,6 @@ public class StringTools {
      * String message = StringTools.format(ptn, params);
      * </pre>
      *
-     * @author zhaohuihua
      * @param string 原字符串
      * @param params 参数键值对
      * @return 参数替换后的字符串
@@ -390,7 +432,6 @@ public class StringTools {
      * String message = StringTools.format(ptn, "nickName", user.getNickName(), "phone", user.getPhone());
      * </pre>
      *
-     * @author zhaohuihua
      * @param string 原字符串
      * @param params 参数键值对, 参数个数必须是2的倍数
      * @return 参数替换后的字符串
@@ -401,16 +442,38 @@ public class StringTools {
         }
 
         if (params.length % 2 != 0) {
-            throw new IllegalArgumentException("参数必须是键值对, 参数个数必须是2的倍数");
+            throw new IllegalArgumentException("The parameter must be a key value pair.");
         }
 
-        Map<String, Object> map = new HashMap<String, Object>();
-        for (int i = 0; i < params.length;) {
+        Map<String, Object> map = new HashMap<>();
+        for (int i = 0; i < params.length; ) {
             map.put(params[i++].toString(), params[i++]);
         }
         return format(string, map);
     }
 
+    /**
+     * 求目标字符在原字符串中最先出现的位置
+     *
+     * @param string 原字符串
+     * @param chars 目标字符
+     * @return 位置 (均未找到时返回-1)
+     */
+    public static int charIndex(String string, char... chars) {
+        if (string == null || string.length() == 0 || chars == null || chars.length == 0) {
+            return -1;
+        }
+        char[] sChars = string.toCharArray();
+        for (int i = 0; i < sChars.length; i++) {
+            for (int j = 0; j < chars.length; j++) {
+                if (sChars[i] == chars[j]) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
     /**
      * 拆分字符串, 以竖杠|为分隔符<br>
      * 每一个子字符串都已经trim()过了<br>
@@ -456,7 +519,7 @@ public class StringTools {
             return new String[0];
         }
         if (chars == null || chars.length == 0) {
-            return new String[] { string };
+            return new String[] {string};
         }
         List<String> list = splits(string, trim, chars);
         return list == null ? null : list.toArray(new String[0]);
@@ -497,7 +560,7 @@ public class StringTools {
             return new String[0];
         }
         if (delimiter == null || delimiter.length() == 0) {
-            return new String[] { string };
+            return new String[] {string};
         }
         List<String> list = splits(string, trim, delimiter);
         return list == null ? null : list.toArray(new String[0]);
@@ -544,6 +607,23 @@ public class StringTools {
      * @return 拆分后的字符串数组
      */
     public static List<String> splits(String string, boolean trim, char... chars) {
+        return splits(string, trim, false, chars);
+    }
+
+    /**
+     * 按指定分隔符拆分字符串<br>
+     * splits("aa|bb|cc", '|') --&gt; [aa, bb, cc]<br>
+     * splits("aa|bb||cc", '|') --&gt; [aa, bb, , cc]<br>
+     * splits("aa\\|bb||cc|\\\\d", true, false, '|') --&gt; ["aa\\", "bb", "", "cc", "\\\\d"]<br>
+     * splits("aa\\|bb||cc|\\\\d", true, true, '|') --&gt; ["aa|bb", "", "cc", "\\d"]
+     *
+     * @param string 原字符串
+     * @param trim 每一个子字符串是否执行trim()
+     * @param escape 是否使用转义 (分隔符前面带反斜杠\当前普通字符)
+     * @param chars 分隔符
+     * @return 拆分后的字符串数组
+     */
+    public static List<String> splits(String string, boolean trim, boolean escape, char... chars) {
         if (string == null) {
             return null;
         }
@@ -551,25 +631,49 @@ public class StringTools {
             return new ArrayList<>();
         }
         if (chars == null || chars.length == 0) {
-            return new ArrayList<>(Collections.singletonList(string));
+            List<String> list = new ArrayList<>();
+            list.add(string);
+            return list;
+        }
+        Map<Character, ?> separatorMaps = new HashMap<>();
+        Map<Character, Character> escapeMaps = new HashMap<>();
+        escapeMaps.put('\\', '\\');
+        for (char c : chars) {
+            separatorMaps.put(c, null);
+            if (escape) {
+                if (c == '\r') {
+                    escapeMaps.put('r', '\r');
+                } else if (c == '\n') {
+                    escapeMaps.put('n', '\n');
+                } else if (c == '\t') {
+                    escapeMaps.put('t', '\t');
+                } else if (c == '\f') {
+                    escapeMaps.put('f', '\f');
+                } else {
+                    escapeMaps.put(c, c);
+                }
+            }
         }
         List<String> list = new ArrayList<>();
         StringBuilder buffer = new StringBuilder();
-        char[] textChars = string.toCharArray();
         boolean lastIsSplitChar = false;
-        for (int i = 0; i < textChars.length; i++) {
-            char c = textChars[i];
-            boolean isSplitChar = false;
-            for (int j = 0; j < chars.length; j++) {
-                if (c == chars[j]) {
-                    isSplitChar = true;
-                    break;
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (escape && c == '\\' && i < z - 1) {
+                char next = string.charAt(++i);
+                if (escapeMaps.containsKey(next)) {
+                    // 是转义字符 (只加转换后的字符, 吞掉斜杠)
+                    buffer.append(escapeMaps.get(next));
+                    lastIsSplitChar = false;
+                    continue;
+                } else {
+                    // 斜杠后面不是转义字符, 补上斜杠, 继续处理下一字符
+                    buffer.append(c);
+                    c = next;
                 }
             }
-            if (!isSplitChar) {
-                buffer.append(c);
-                lastIsSplitChar = false;
-            } else {
+            if (separatorMaps.containsKey(c)) {
+                // 遇到分隔符
                 if (trim) {
                     list.add(trim(buffer.toString()));
                 } else {
@@ -577,6 +681,10 @@ public class StringTools {
                 }
                 buffer.setLength(0);
                 lastIsSplitChar = true;
+            } else {
+                // 不是分隔符
+                buffer.append(c);
+                lastIsSplitChar = false;
             }
         }
         if (buffer.length() > 0) {
@@ -655,7 +763,7 @@ public class StringTools {
 
     /**
      * 拆分字段名<br>
-     * 
+     *
      * @param fields 字段名字符串
      * @return 字段名数组
      * @deprecated move to {@link StringParser#splitFields(String)}
@@ -667,7 +775,7 @@ public class StringTools {
 
     /**
      * 拆分字段名
-     * 
+     *
      * @param fields 字段名字符串
      * @return 字段名数组
      * @deprecated move to {@link StringParser#splitsFields(String)}
@@ -682,13 +790,12 @@ public class StringTools {
      * 如果未超过指定长度则返回原字符, length小于20时省略最后部分而不是中间部分<br>
      * 如: 诺贝尔奖是以瑞典著名的 ... 基金创立的
      *
-     * @author zhaohuihua
      * @param string 长文本
      * @param length 指定长度
      * @return 省略中间部分的字符
      */
     public static String ellipsis(String string, int length) {
-        if (string == null || string.length() <= length || length == 0) {
+        if (string == null || string.length() <= length || length <= 0) {
             return string;
         }
         String flag = " ... ";
@@ -703,7 +810,7 @@ public class StringTools {
 
     /**
      * 连接字符串
-     * 
+     *
      * @param parts 字符串片断
      * @return 完整字符串
      * @since 5.0.0
@@ -713,8 +820,7 @@ public class StringTools {
             return null;
         }
         StringBuilder buffer = new StringBuilder();
-        for (int i = 0, len = parts.length; i < len; i++) {
-            String part = parts[i];
+        for (String part : parts) {
             if (part != null && part.length() > 0) {
                 buffer.append(part);
             }
@@ -724,7 +830,7 @@ public class StringTools {
 
     /**
      * 连接字符串
-     * 
+     *
      * @param c 分隔符
      * @param parts 字符串片断
      * @return 完整字符串
@@ -735,7 +841,7 @@ public class StringTools {
 
     /**
      * 连接字符串
-     * 
+     *
      * @param c 分隔符
      * @param parts 字符串片断
      * @param start 起始位置
@@ -748,7 +854,7 @@ public class StringTools {
 
     /**
      * 连接字符串
-     * 
+     *
      * @param c 分隔符
      * @param prefix 前缀
      * @param parts 字符串片断
@@ -797,7 +903,7 @@ public class StringTools {
 
     /**
      * 指定字符串是不是以空白字符开头
-     * 
+     *
      * @param string 指定字符串
      * @return 是不是以空白字符开头
      * @since 5.0.0
@@ -811,7 +917,7 @@ public class StringTools {
 
     /**
      * 指定字符串是不是以空白字符结尾
-     * 
+     *
      * @param string 指定字符串
      * @return 是不是以空白字符结尾
      * @since 5.0.0
@@ -826,7 +932,7 @@ public class StringTools {
     /**
      * 是不是英文空白字符<br>
      * 不能用Character.isWhitespace(' '); 因为中文空格也会返回true
-     * 
+     *
      * @param c 指定字符
      * @return 是不是空白字符
      * @since 5.0.0
@@ -839,7 +945,7 @@ public class StringTools {
 
     /**
      * 指定字符串是否全是英文空白字符
-     * 
+     *
      * @param string 指定字符串
      * @return 是不是空白字符串
      * @since 5.2.0
@@ -896,7 +1002,40 @@ public class StringTools {
 
     /**
      * 删除左右两侧的指定字符
-     * 
+     *
+     * @param string 原文本
+     * @param chars 待删除的字符
+     * @return 删除后的字符串
+     */
+    public static String trimLeftRight(String string, char... chars) {
+        return removeLeftRight(string, true, true, chars);
+    }
+
+    /**
+     * 删除左侧的指定字符
+     *
+     * @param string 原文本
+     * @param chars 待删除的字符
+     * @return 删除后的字符串
+     */
+    public static String trimLeft(String string, char... chars) {
+        return removeLeftRight(string, true, false, chars);
+    }
+
+    /**
+     * 删除右侧的指定字符
+     *
+     * @param string 原文本
+     * @param chars 待删除的字符
+     * @return 删除后的字符串
+     */
+    public static String trimRight(String string, char... chars) {
+        return removeLeftRight(string, false, true, chars);
+    }
+
+    /**
+     * 删除左右两侧的指定字符
+     *
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
@@ -904,43 +1043,52 @@ public class StringTools {
      */
     @Deprecated
     public static String remove(String string, char... chars) {
-        return removeLeftRight(string, chars);
+        return trimLeftRight(string, chars);
     }
 
     /**
      * 删除左右两侧的指定字符
-     * 
+     *
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
+     * @deprecated 与removeLeftRight(string, int, int)冲突, 改为trimLeftRight(String string, char... chars)
      */
+    @Deprecated
     public static String removeLeftRight(String string, char... chars) {
         return removeLeftRight(string, true, true, chars);
     }
 
     /**
      * 删除左侧的指定字符
-     * 
+     *
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
+     * @deprecated 与removeLeft(string, int)冲突, 改为trimLeft(String string, char... chars)
      */
+    @Deprecated
     public static String removeLeft(String string, char... chars) {
         return removeLeftRight(string, true, false, chars);
     }
 
     /**
      * 删除右侧的指定字符
-     * 
+     *
      * @param string 原文本
      * @param chars 待删除的字符
      * @return 删除后的字符串
+     * @deprecated 与removeRight(string, int)冲突, 改为trimRight(String string, char... chars)
      */
+    @Deprecated
     public static String removeRight(String string, char... chars) {
         return removeLeftRight(string, false, true, chars);
     }
 
     private static String removeLeftRight(String string, boolean left, boolean right, char... chars) {
+        if (string == null) {
+            return null;
+        }
         char[] value = string.toCharArray();
         int length = value.length;
         int start = 0;
@@ -960,7 +1108,7 @@ public class StringTools {
 
     /**
      * 删除中间的字符
-     * 
+     *
      * @param string 原文本
      * @param start 左侧开始删除的数量(左侧保留的数量)
      * @param end 删除到右侧的位置(右侧保留的数量)
@@ -974,7 +1122,7 @@ public class StringTools {
 
     /**
      * 删除中间的字符
-     * 
+     *
      * @param string 原文本
      * @param start 左侧开始删除的数量(左侧保留的数量)
      * @param end 删除到右侧的位置(右侧保留的数量)
@@ -997,7 +1145,7 @@ public class StringTools {
 
     /**
      * 删除左右两侧指定数量的字符
-     * 
+     *
      * @param string 原文本
      * @param left 左侧删除的数量
      * @param right 右侧删除的数量
@@ -1022,7 +1170,7 @@ public class StringTools {
     /**
      * 删除前缀, 如果没有指定的前缀就返回原字符串<br>
      * removePrefix("userNameEquals", "userName") = "Equals"
-     * 
+     *
      * @param string 待处理的字符串
      * @param prefix 指定前缀
      * @return 处理后的字符串
@@ -1040,7 +1188,7 @@ public class StringTools {
     /**
      * 删除后缀, 如果没有指定的后缀就返回原字符串<br>
      * removeSuffix("userNameEquals", "Equals") = "userName"
-     * 
+     *
      * @param string 待处理的字符串
      * @param suffix 指定后缀
      * @return 处理后的字符串
@@ -1059,7 +1207,7 @@ public class StringTools {
     /**
      * 删除前缀和后缀, 如果没有指定的前缀和后缀就返回原字符串<br>
      * removePrefixSuffix("userNameEquals", "user", "Equals") = "Name"
-     * 
+     *
      * @param string 待处理的字符串
      * @param prefix 指定前缀
      * @param suffix 指定后缀
@@ -1085,7 +1233,7 @@ public class StringTools {
      * 删除前缀(删除到最后一个指定字符为止)<br>
      * removePrefixAt("userName$Equals", '$') = "Equals"<br>
      * removePrefixAt("user$Name$Equals", '$') = "Equals"<br>
-     * 
+     *
      * @param string 源字符串
      * @param c 指定字符
      * @return 处理后的字符串, 如果没有指定字符就返回源字符串
@@ -1102,7 +1250,7 @@ public class StringTools {
      * 删除前缀(删除到最后一个指定字符串为止)<br>
      * removePrefixAt("userName$$Equals", "$$") = "Equals"<br>
      * removePrefixAt("user$$Name$$Equals","$$") = "Equals"<br>
-     * 
+     *
      * @param string 源字符串
      * @param c 指定字符串
      * @return 处理后的字符串, 如果没有指定字符串就返回源字符串
@@ -1120,7 +1268,7 @@ public class StringTools {
      * 删除后缀(从第一个指定字符开始删除)<br>
      * removeSuffixAt("userName$Equals", '$') = "userName"<br>
      * removeSuffixAt("user$Name$Equals", '$') = "user"<br>
-     * 
+     *
      * @param string 源字符串
      * @param c 指定字符
      * @return 处理后的字符串, 如果没有指定字符就返回源字符串
@@ -1137,7 +1285,7 @@ public class StringTools {
      * 删除后缀(从第一个指定字符串开始删除)<br>
      * removeSuffixAt("userName$$Equals", "$$") = "userName"<br>
      * removeSuffixAt("user$$Name$$Equals", "$$") = "user"<br>
-     * 
+     *
      * @param string 源字符串
      * @param c 指定字符串
      * @return 处理后的字符串, 如果没有指定字符串就返回源字符串
@@ -1154,12 +1302,15 @@ public class StringTools {
     /**
      * 删除指定的子字符串<br>
      * 如: removeSubStrings("11223344", "22", "33") 输出 1144<br>
-     * 
+     *
      * @param string 源字符串
      * @param sub 待删除的字符串
      * @return 删除后的字符串
      */
     public static String removeSubStrings(String string, String... sub) {
+        if (string == null || sub == null || sub.length == 0) {
+            return string;
+        }
         for (String s : sub) {
             string = removeSubString(string, s);
         }
@@ -1169,12 +1320,15 @@ public class StringTools {
     /**
      * 删除指定的子字符串<br>
      * 如: removeSubString("11223344", "22") 输出 113344<br>
-     * 
+     *
      * @param string 源字符串
      * @param sub 待删除的字符串
      * @return 删除后的字符串
      */
     public static String removeSubString(String string, String sub) {
+        if (string == null || sub == null || sub.length() == 0) {
+            return string;
+        }
         StringBuilder buffer = new StringBuilder();
         int index = 0;
         int startIndex;
@@ -1193,24 +1347,27 @@ public class StringTools {
     /**
      * 删除指定的字符<br>
      * 如: removeChars("11223344", '2','3') 输出 1144<br>
-     * 
+     *
      * @param string 源字符串
      * @param chars 待删除的字符
      * @return 删除后的字符串
      */
     public static String removeChars(String string, char... chars) {
+        if (string == null || chars == null || chars.length == 0) {
+            return string;
+        }
         StringBuilder buffer = new StringBuilder();
         char[] sources = string.toCharArray();
-        for (int i = 0; i < sources.length; i++) {
+        for (char source : sources) {
             int index = -1;
             for (int j = 0; j < chars.length; j++) {
-                if (sources[i] == chars[j]) {
+                if (source == chars[j]) {
                     index = j;
                     break;
                 }
             }
             if (index < 0) {
-                buffer.append(sources[i]);
+                buffer.append(source);
             }
         }
         return buffer.toString();
@@ -1220,13 +1377,16 @@ public class StringTools {
      * 删除成对的符号及包含在中间的内容<br>
      * 如: removeInPairedSymbol("111<!--xxx-->222<!--xxx-->333", "<!--", "-->") 输出 111222333<br>
      * 如: removeInPairedSymbol("111/&#42;xxx&#42;/222/&#42;xxx&#42;/333", "/&#42;", "&#42;/") 输出 111222333<br>
-     * 
+     *
      * @param string 源字符串
      * @param leftSymbol 左侧的符号
      * @param rightSymbol 右侧的符号
      * @return 删除后的字符串
      */
     public static String removeInPairedSymbol(String string, String leftSymbol, String rightSymbol) {
+        if (string == null || leftSymbol == null || rightSymbol == null) {
+            return null;
+        }
         StringBuilder buffer = new StringBuilder();
         int index = 0;
         int leftIndex;
@@ -1250,7 +1410,7 @@ public class StringTools {
      * 获取成对符号之间的内容<br>
      * 如: getSubstringInPairedSymbol("111<!--222-->333", "<!--", "-->") 输出 222<br>
      * 如: getSubstringInPairedSymbol("111/&#42;222&#42;/333", "/&#42;", "&#42;/") 输出 222<br>
-     * 
+     *
      * @param string 源字符串
      * @param leftSymbol 左侧的符号
      * @param rightSymbol 右侧的符号
@@ -1258,6 +1418,9 @@ public class StringTools {
      * @since 5.0.0
      */
     public static String getSubstringInPairedSymbol(String string, String leftSymbol, String rightSymbol) {
+        if (string == null || leftSymbol == null || rightSymbol == null) {
+            return null;
+        }
         int startIndex = string.indexOf(leftSymbol);
         if (startIndex < 0) {
             return null;
@@ -1270,11 +1433,45 @@ public class StringTools {
         return string.substring(startIndex, endIndex);
     }
 
+    /**
+     * 替换相同长度的前缀
+     *
+     * @param string 源字符串
+     * @param prefix 新的前缀
+     * @return 替换后的字符串
+     */
+    public static String replacePrefix(String string, String prefix) {
+        if (string == null || prefix == null) {
+            return string;
+        }
+        if (prefix.length() == 0 || string.length() < prefix.length()) {
+            return string;
+        }
+        return prefix + removeLeftRight(string, prefix.length(), 0);
+    }
+
+    /**
+     * 替换相同长度的后缀
+     *
+     * @param string 源字符串
+     * @param suffix 新的后缀
+     * @return 替换后的字符串
+     */
+    public static String replaceSuffix(String string, String suffix) {
+        if (string == null || suffix == null) {
+            return string;
+        }
+        if (suffix.length() == 0 || string.length() < suffix.length()) {
+            return string;
+        }
+        return removeLeftRight(string, 0, suffix.length()) + suffix;
+    }
+
     /**
      * 字符串替换(非正则)<br>
      * 例如: \t替换为空格, \r\n替换为\n<br>
      * StringTools.replace("abc\tdef\r\nxyz", "\t", " ", "\r\n", "\n");
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @return 替换后的字符串
@@ -1299,7 +1496,7 @@ public class StringTools {
      * 字符串替换(非正则)<br>
      * 例如: \t替换为空格, \r\n替换为\n<br>
      * StringTools.replace("abc\tdef\r\nxyz", "\t", " ", "\r\n", "\n");
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1320,7 +1517,7 @@ public class StringTools {
      * 字符串替换(非正则)<br>
      * 例如: \t替换为空格, \r\n替换为\n<br>
      * StringTools.replace("abc\tdef\r\nxyz", "\t", " ", "\r\n", "\n");
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1338,7 +1535,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1358,7 +1555,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1382,7 +1579,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param patterns 替换规则
      * @since 5.0.0
@@ -1406,7 +1603,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param pattern 替换规则
      * @param replacement 替换目标
@@ -1442,7 +1639,7 @@ public class StringTools {
 
     /**
      * 字符串替换(非正则)
-     * 
+     *
      * @param string 源字符串
      * @param pattern 替换规则
      * @param replacement 替换目标
@@ -1485,7 +1682,7 @@ public class StringTools {
         }
 
         List<KeyString> list = new ArrayList<>();
-        for (int i = 0; i < patterns.length;) {
+        for (int i = 0; i < patterns.length; ) {
             list.add(new KeyString(patterns[i++], patterns[i++]));
         }
 
@@ -1493,8 +1690,8 @@ public class StringTools {
     }
 
     private static boolean inArray(char c, char[] array) {
-        for (int i = 0, len = array.length; i < len; i++) {
-            if (array[i] == c) {
+        for (char item : array) {
+            if (c == item) {
                 return true;
             }
         }
@@ -1519,7 +1716,7 @@ public class StringTools {
     /**
      * 左侧补字符<br>
      * pad("12345", '_', 10) 返回 _____12345<br>
-     * 
+     *
      * @param string 原字符串
      * @param c 补充的字符
      * @param length 目标长度
@@ -1532,7 +1729,7 @@ public class StringTools {
     /**
      * 左侧或右侧补字符<br>
      * pad("12345", '_', false, 10) 返回 12345_____<br>
-     * 
+     *
      * @param string 原字符串
      * @param c 补充的字符
      * @param left 左侧补(true)还是右侧补(false)
@@ -1551,7 +1748,7 @@ public class StringTools {
 
     /**
      * 统计子字符串出现次数
-     * 
+     *
      * @param string 源字符串
      * @param substring 子字符串
      * @return 次数
@@ -1566,16 +1763,16 @@ public class StringTools {
 
     /**
      * 统计字符出现次数
-     * 
+     *
      * @param string 源字符串
      * @param character 目标字符
      * @return 次数
      */
     public static int countCharacter(String string, char character) {
         int count = 0;
-        char[] chars = string.toCharArray();
-        for (int i = 0, z = chars.length; i < z; i++) {
-            if (chars[i] == character) {
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (c == character) {
                 count++;
             }
         }
@@ -1614,7 +1811,7 @@ public class StringTools {
 
     /**
      * 判断指定字符串是不是字段名称
-     * 
+     *
      * @param string 字符串
      * @return 是不是字段名称
      */
@@ -1638,9 +1835,9 @@ public class StringTools {
     }
 
     private static boolean isFieldString(CharSequence string) {
-        char[] chars = string.toString().toCharArray();
-        for (int i = 0; i < chars.length; i++) {
-            if (!isFieldChar(chars[i])) {
+        for (int i = 0, z = string.length(); i < z; i++) {
+            char c = string.charAt(i);
+            if (!isFieldChar(c)) {
                 return false;
             }
         }
@@ -1649,13 +1846,13 @@ public class StringTools {
 
     /**
      * 是不是单词字符
-     * 
+     *
      * @param c 字符
      * @return 是不是单词字符
      */
     private static boolean isFieldChar(char c) {
         return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
-        // 支持这些特殊字符
+                // 支持这些特殊字符
                 || c == '$' || c == '_' || c == '-' || c == ':' || c == '.';
     }
 
@@ -1699,7 +1896,7 @@ public class StringTools {
 
     /**
      * 是否存在指定单词
-     * 
+     *
      * @param string 字符串
      * @param keyword 指定单词
      * @return 是否存在指定单词
@@ -1737,7 +1934,7 @@ public class StringTools {
      *
      * @param string 手机号码邮箱或名字
      * @return 隐藏后的字符串, 如: 139****1382, zh****ua@126.com, <br>
-     *         黄山-〇山, 昆仑山-〇〇山, 黄山毛峰-〇〇毛峰
+     * 黄山-〇山, 昆仑山-〇〇山, 黄山毛峰-〇〇毛峰
      */
     public static String hidden(String string) {
         if (string == null || string.length() == 0) {
@@ -1793,7 +1990,7 @@ public class StringTools {
     /**
      * 获取UTF8编码格式的长度<br>
      * 0-127长度为1, 如英文字符; 128~2047长度为2, 如ð; emoji为4, 如😎; 其他为3, 如中文字符
-     * 
+     *
      * @param string 字符串
      * @return 长度
      */
@@ -1817,10 +2014,10 @@ public class StringTools {
 
     /**
      * 如果全是大写字母, 则转换为小写字母
-     * 
+     *
      * @param name 待转换的名称
      * @return 转换后的名称
-     * @deprecated 改为{@link NamingTools#toLowerCaseIfAllUpperCase(String)}
+     * @deprecated 改为 {@link NamingTools#toLowerCaseIfAllUpperCase(String)}
      */
     @Deprecated
     public static String toLowerCaseIfAllUpperCase(String name) {
@@ -1831,10 +2028,9 @@ public class StringTools {
      * 转换为驼峰命名法格式<br>
      * 如: user_name = userName, iuser_service = iuserService, i_user_service = iUserService
      *
-     * @author zhaohuihua
      * @param name 待转换的名称
      * @return 驼峰命名法名称
-     * @deprecated 改为{@link NamingTools#toCamelString(String)}
+     * @deprecated 改为 {@link NamingTools#toCamelString(String)}
      */
     @Deprecated
     public static String toCamelNaming(String name) {
@@ -1846,11 +2042,10 @@ public class StringTools {
      * 如startsWithUpperCase=true时:<br>
      * user_name = UserName, iuser_service = IuserService, i_user_service = IUserService
      *
-     * @author zhaohuihua
      * @param name 待转换的名称
      * @param startsWithUpperCase 是否以大写字母开头
      * @return 驼峰命名法名称
-     * @deprecated 改为{@link NamingTools#toCamelString(String, boolean)}
+     * @deprecated 改为 {@link NamingTools#toCamelString(String, boolean)}
      */
     @Deprecated
     public static String toCamelNaming(String name, boolean startsWithUpperCase) {
@@ -1862,10 +2057,9 @@ public class StringTools {
      * 如: userName = user_name, SiteURL = site_url, IUserService = iuser_service<br>
      * user$Name = user$name, user_Name = user_name, user name = user_name, md5String = md5_string
      *
-     * @author zhaohuihua
      * @param name 待转换的名称
      * @return 下划线命名法名称
-     * @deprecated 改为{@link NamingTools#toUnderlineNaming(String)}
+     * @deprecated 改为 {@link NamingTools#toUnderlineString(String)}
      */
     @Deprecated
     public static String toUnderlineNaming(String name) {
@@ -1893,7 +2087,7 @@ public class StringTools {
      * @param string the String to capitalize, may be null
      * @return the capitalized String, {@code null} if null String input
      * @since 2.0
-     * @deprecated 改为{@link NamingTools#capitalize(String)}
+     * @deprecated 改为 {@link NamingTools#capitalize(String)}
      */
     @Deprecated
     public static String capitalize(final String string) {
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
index ad29a4bec3f6021dd818008052a82abcb91a175e..3baab945b97cacb34f7d24a09e13dae200923976 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/VerifyTools.java
@@ -22,45 +22,45 @@ public class VerifyTools {
     /**
      * 检查指定的对象是否不为null, 如果为null将抛出ServiceException异常.<br>
      * 用法: VerifyTools.requireNonNull(entity.getEmail(), "email");
-     * 
+     *
      * @param object 待检查的对象
      * @param name 对象的描述或名称
      */
     public static void requireNonNull(Object object, String name) {
         if (object == null) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be null.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be null.");
         }
     }
 
     /**
      * 检查指定的对象是否不为空, 如果为空(或为空字符串)将抛出ServiceException异常.<br>
      * 用法: VerifyTools.requireNotBlank(entity.getEmail(), "email");
-     * 
+     *
      * @param object 待检查的对象
      * @param name 对象的描述或名称
      */
     public static void requireNotBlank(Object object, String name) {
         if (object == null) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be null.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be null.");
         }
         if (isBlank(object)) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be empty.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be empty.");
         }
     }
 
     /**
      * 检查指定的字符串是否不为空, 如果为空(或为空字符串)将抛出ServiceException异常.<br>
      * 用法: VerifyTools.requireNotBlank(entity.getEmail(), "email");
-     * 
+     *
      * @param string 待检查的对象
      * @param name 对象的描述或名称
      */
     public static void requireNotBlank(String string, String name) {
         if (string == null) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be null.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be null.");
         }
         if (isBlank(string)) {
-            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED, name + " must not be empty.");
+            throw new ServiceException(ResultCode.PARAMETER_IS_REQUIRED).setDetails(name + " must not be empty.");
         }
     }
 
@@ -80,7 +80,7 @@ public class VerifyTools {
     /**
      * 判断字符串是否为空<br>
      * 零长度的字符串将被判定为空
-     * 
+     *
      * @param string 目标对象
      * @return true or false
      */
@@ -90,7 +90,7 @@ public class VerifyTools {
 
     /**
      * 判断字符串是否为非空
-     * 
+     *
      * @param string 目标对象
      * @return true or false
      */
@@ -101,7 +101,7 @@ public class VerifyTools {
     /**
      * 判断对象是否为空<br>
      * 零长度的字符串, length=0的数组, 空的Collection, 空的Map, 空的Iterable都将被判定为空
-     * 
+     *
      * @param object 目标对象
      * @return true or false
      */
@@ -130,7 +130,7 @@ public class VerifyTools {
 
     /**
      * 判断对象是否为非空
-     * 
+     *
      * @param object 目标对象
      * @return true or false
      */
@@ -190,7 +190,7 @@ public class VerifyTools {
 
     /**
      * 判断对象是否存在于列表中
-     * 
+     *
      * @param object 目标对象
      * @param objects 列表
      * @return 是否存在
@@ -214,7 +214,7 @@ public class VerifyTools {
 
     /**
      * 判断对象是否不存在于列表中
-     * 
+     *
      * @param object 目标对象
      * @param objects 列表
      * @return 是否不存在
@@ -264,4 +264,76 @@ public class VerifyTools {
     public static boolean isChanged(Object value, Object older) {
         return isNotBlank(value) && notEquals(value, older);
     }
+
+    /** 是不是JsonObject字符串 **/
+    public static boolean isJsonObjectString(String value) {
+        return checkJsonObjectString(value);
+    }
+
+    /** 是不是JsonObject字符串 **/
+    public static boolean isJsonObjectString(Object value) {
+        return value instanceof String && checkJsonObjectString((String) value);
+    }
+
+    // 以{开头, 以}结尾, 且中间有冒号
+    private static boolean checkJsonObjectString(String string) {
+        boolean passed = checkStringFeature(string, '{', '}');
+        if (!passed) {
+            return false;
+        } else {
+            String content = StringTools.removeLeftRight(string, 1, 1).trim();
+            return content.length() == 0 || content.indexOf(':') > 0;
+        }
+    }
+
+    /** 是不是JsonArray字符串 **/
+    public static boolean isJsonArrayString(String value) {
+        return checkJsonArrayString(value);
+    }
+
+    /** 是不是JsonArray字符串 **/
+    public static boolean isJsonArrayString(Object value) {
+        return value instanceof String && checkJsonArrayString((String) value);
+    }
+
+    private static boolean checkJsonArrayString(String string) {
+        return checkStringFeature(string, '[', ']');
+    }
+
+    /**
+     * 是不是XML字符串 (只是简单判断以&lt;开头且以&gt;结尾)
+     *
+     * @param value 判断目标
+     * @return 判断结果
+     * @since 5.5.13
+     */
+    public static boolean isXmlString(String value) {
+        return checkXmlString(value);
+    }
+
+    /**
+     * 是不是XML字符串 (只是简单判断以&lt;开头且以&gt;结尾)
+     *
+     * @param value 判断目标
+     * @return 判断结果
+     * @since 5.5.13
+     */
+    public static boolean isXmlString(Object value) {
+        return value instanceof String && checkXmlString((String) value);
+    }
+
+    private static boolean checkXmlString(String string) {
+        return checkStringFeature(string, '<', '>');
+    }
+
+    private static boolean checkStringFeature(String value, char first, char last) {
+        if (value == null) {
+            return false;
+        }
+        String string = value.trim();
+        if (string.length() > 0 && string.charAt(0) == first && string.charAt(string.length() - 1) == last) {
+            return true;
+        }
+        return false;
+    }
 }
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/VersionCodeTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/VersionCodeTools.java
index 225be5cf346010af6c04cdeb81ac17fbd8ad2580..6723431a4bd75ac7db85cff3ee37d26b32ad76b2 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/VersionCodeTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/VersionCodeTools.java
@@ -139,8 +139,8 @@ public class VersionCodeTools {
     }
 
     private static int compareVersionNumber(String source, String target) {
-        source = StringTools.removeLeft(source, '0');
-        target = StringTools.removeLeft(target, '0');
+        source = StringTools.trimLeft(source, '0');
+        target = StringTools.trimLeft(target, '0');
         if (source.length() > target.length()) {
             return 1;
         } else if (source.length() < target.length()) {
diff --git a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
index 7b009ccedcdb1787e976157bc8b51cf83b8cc652..e37daac2f1ebb8b71d03b8d3a2ba235cff113004 100644
--- a/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
+++ b/able/src/main/java/com/gitee/qdbp/tools/utils/XmlTools.java
@@ -17,6 +17,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.i18n.LocaleTools;
+import com.gitee.qdbp.able.matches.StringMatcher;
 import com.gitee.qdbp.able.result.IResultMessage;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -34,40 +35,11 @@ import org.xml.sax.SAXException;
 public class XmlTools {
 
     public static Map<String, Object> xmlToJson(String xml) {
-        return xmlToJson(xml, "array,list");
+        return xmlToJson(xml, ARRAY_NAME);
     }
 
     public static Map<String, Object> xmlToJson(String xml, String arrayNodeNames) {
-        XmlToJsonOptions options = new XmlToJsonOptions();
-
-        // 是否使用驼峰命名
-        // 如果为true, 则XML中的<USER_LIST>将会转换为userList
-        options.setUseCamelNaming(true);
-
-        // 是否使用属性容器: 
-        // 如果为true, 属性都集中放在一个容器中; 
-        // 如果为false则不加容器, 与子元素放在一起; 此时如果属性名与子节点名相同, 子节点会覆盖属性名
-        options.setUseAttrContainer(false);
-        // options.setAttrContainerName("$attrs"); // useAttrContainer=false时无效
-
-        // 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称
-        // <node x="111" y="222">333</node> --> { node:{ x:111, y:222, text:333 } }
-        // 上例, 由于node有属性, 于是node变成了一个map节点, 那么这里的333必须要有字段名, 否则无法保存
-        options.setTextContentName("text");
-
-        // 数组节点名称
-        // <array> <aa>1</aa> <aa>2</aa> </array>
-        // 上例: array下全是同名节点, 将会合并为数组 { aa:[1,2] }
-        // <array> <aa>1</aa> </array>
-        // 上例: array下只有一个节点, 也会合并为数组 { aa:[1] }
-        // <array> <aa>1</aa> <aa>2</aa> <bb>3</bb> 444 </array>
-        // 上例: array下存在不同名的节点, 将会包装为数组 { array:[ {aa:1}, {aa:2}, {bb:3}, 444 ] }
-        // <array> 1 </array>
-        // 上例: array下只有一个文本节点, 也会包装为数组 { array:[1] }
-        if (VerifyTools.isNotBlank(arrayNodeNames)) {
-            options.setArrayNodeName(arrayNodeNames);
-        }
-
+        XmlToJsonOptions options = XmlToJsonOptions.defaults().setArrayNodeName(arrayNodeNames);
         return xmlToJson(xml, options);
     }
 
@@ -95,7 +67,7 @@ public class XmlTools {
         return container;
     }
 
-    // { parent: { array: [ {aa:1},{aa:2} ] } } 提供为 { parent: { aa:[1,2] } }
+    // { parent: { array: [ {aa:1},{aa:2} ] } } 提升为 { parent: { aa:[1,2] } }
     @SuppressWarnings("unchecked")
     private static void upgradeArraySameNodes(Map<String, Object> map, XmlToJsonOptions o) {
         for (Map.Entry<String, Object> entry : map.entrySet()) {
@@ -152,10 +124,29 @@ public class XmlTools {
     }
 
     private static String resolveMapKey(String nodeName, XmlToJsonOptions options) {
-        if (options == null || !options.isUseCamelNaming()) {
+        if (options == null) {
             return nodeName;
+        } else if (!options.isUseCamelNaming()) {
+            return handleNamespacePrefix(nodeName, options);
         } else {
-            return NamingTools.toCamelString(nodeName);
+            return NamingTools.toCamelString(handleNamespacePrefix(nodeName, options));
+        }
+    }
+
+    private static String handleNamespacePrefix(String nodeName, XmlToJsonOptions options) {
+        NamespacePrefixStrategy strategy = options == null ? null : options.getNamespacePrefixStrategy();
+        if (strategy == null) {
+            return nodeName;
+        }
+        switch (strategy) {
+            case REMOVE:
+                return StringTools.removePrefixAt(nodeName, ":");
+            case NORMALIZING:
+                return StringTools.replace(nodeName, ":", "_");
+            case DOLLAR_SEPARATOR:
+                return StringTools.replace(nodeName, ":", "$");
+            default:
+                return nodeName;
         }
     }
 
@@ -409,6 +400,136 @@ public class XmlTools {
 
     public static class XmlToJsonOptions {
 
+        /**
+         * 推荐选项 (解析结果有可能与旧版本不一致)<br>
+         * 1. 字段使用驼峰命名<br>
+         * -- &lt;USER_LIST>将会转换为userList<br>
+         * 2. 属性不使用容器, 属性与子元素放在一起<br>
+         * -- 如果属性名与子节点名相同, 子节点会覆盖属性名<br>
+         * 3. 文本内容的字段名称为text (如果节点有属性, 则节点中的文本内容需要有字段名称)<br>
+         * -- &lt;node x="111" y="222">333&lt;/node> --> { node:{ x:111, y:222, text:333 } }<br>
+         * 4. 数组节点名称为array,list<br>
+         * -- &lt;array> &lt;aa>1&lt;/aa> &lt;aa>2&lt;/aa> &lt;/array> --> { aa:[1,2] }<br>
+         * 5. 命名空间前缀处理策略为移除<br>
+         * -- &lt;soap:Body>转换为为body
+         * @return 推荐选项
+         * @since 5.5.10
+         */
+        public static XmlToJsonOptions recommend() {
+            XmlToJsonOptions options = defaults();
+            // REMOTE: 移除, <soap:Body>转换为body
+            options.setNamespacePrefixStrategy(NamespacePrefixStrategy.REMOVE);
+            return options;
+        }
+
+        /**
+         * 默认选项 (与旧版本保持一致)<br>
+         * 1. 字段使用驼峰命名<br>
+         * -- &lt;USER_LIST>将会转换为userList<br>
+         * 2. 属性不使用容器, 属性与子元素放在一起<br>
+         * -- 如果属性名与子节点名相同, 子节点会覆盖属性名<br>
+         * 3. 文本内容的字段名称为text (如果节点有属性, 则节点中的文本内容需要有字段名称)<br>
+         * -- &lt;node x="111" y="222">333&lt;/node> --> { node:{ x:111, y:222, text:333 } }<br>
+         * 4. 数组节点名称为array,list<br>
+         * -- &lt;array> &lt;aa>1&lt;/aa> &lt;aa>2&lt;/aa> &lt;/array> --> { aa:[1,2] }<br>
+         * 5. 命名空间前缀处理策略为不处理<br>
+         * -- &lt;soap:Body>转换为为soap:body
+         *
+         * @return 默认选项
+         * @since 5.5.10
+         */
+        public static XmlToJsonOptions defaults() {
+            XmlToJsonOptions options = new XmlToJsonOptions();
+
+            // 是否使用驼峰命名
+            // 如果为true, 则XML中的<USER_LIST>将会转换为userList
+            options.setUseCamelNaming(true);
+
+            // 是否使用属性容器:
+            // 如果为true, 属性都集中放在一个容器中;
+            // 如果为false则不加容器, 与子元素放在一起; 此时如果属性名与子节点名相同, 子节点会覆盖属性名
+            options.setUseAttrContainer(false);
+            // options.setAttrContainerName("$attrs"); // useAttrContainer=false时无效
+
+            // 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称
+            // <node x="111" y="222">333</node> --> { node:{ x:111, y:222, text:333 } }
+            // 上例, 由于node有属性, 于是node变成了一个map节点, 那么这里的333必须要有字段名, 否则无法保存
+            options.setTextContentName("text");
+
+            // 数组节点名称
+            // <array> <aa>1</aa> <aa>2</aa> </array>
+            // 上例: array下全是同名节点, 将会合并为数组 { aa:[1,2] }
+            // <array> <aa>1</aa> </array>
+            // 上例: array下只有一个节点, 也会合并为数组 { aa:[1] }
+            // <array> <aa>1</aa> <aa>2</aa> <bb>3</bb> 444 </array>
+            // 上例: array下存在不同名的节点, 将会包装为数组 { array:[ {aa:1}, {aa:2}, {bb:3}, 444 ] }
+            // <array> 1 </array>
+            // 上例: array下只有一个文本节点, 也会包装为数组 { array:[1] }
+            options.setArrayNodeName(ARRAY_NAME);
+
+            // NONE: 不处理, <soap:Body>转换为soap:body
+            // REMOTE: 移除, <soap:Body>转换为body
+            // NORMALIZING: 标准化处理, <soap:Body>转换为soapBody
+            // DOLLAR_SEPARATOR: 使用$分隔符, <soap:Body>转换为soap$Body
+            options.setNamespacePrefixStrategy(NamespacePrefixStrategy.NONE);
+
+            return options;
+        }
+
+        /**
+         * 保守选项 (尽量少转换多保留)<br>
+         * 1. 字段不使用驼峰命名<br>
+         * -- &lt;USER_LIST>将会转换为USER_LIST<br>
+         * 2. 属性使用容器, 容器名为'$attrs'<br>
+         * -- 如果属性名与子节点名相同, 子节点会覆盖属性名<br>
+         * 3. 文本内容的字段名称为$text (如果节点有属性, 则节点中的文本内容需要有字段名称)<br>
+         * -- &lt;node x="111" y="222">333&lt;/node> --> { node:{ x:111, y:222, $text:333 } }<br>
+         * 4. 数组节点名称为array,list<br>
+         * -- &lt;array> &lt;aa>1&lt;/aa> &lt;aa>2&lt;/aa> &lt;/array> --> { aa:[1,2] }<br>
+         * 5. 命名空间前缀处理策略为不处理<br>
+         * -- &lt;soap:Body>转换为为soap:body
+         *
+         * @return 保守选项
+         * @since 5.5.10
+         */
+        public static XmlToJsonOptions conservative() {
+            XmlToJsonOptions options = new XmlToJsonOptions();
+
+            // 是否使用驼峰命名
+            // 如果为true, 则XML中的<USER_LIST>将会转换为userList
+            options.setUseCamelNaming(false);
+
+            // 是否使用属性容器:
+            // 如果为true, 属性都集中放在一个容器中;
+            // 如果为false则不加容器, 与子元素放在一起; 此时如果属性名与子节点名相同, 子节点会覆盖属性名
+            options.setUseAttrContainer(true);
+            options.setAttrContainerName(ATTR_NAME);
+
+            // 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称
+            // <node x="111" y="222">333</node> --> { node:{ x:111, y:222, text:333 } }
+            // 上例, 由于node有属性, 于是node变成了一个map节点, 那么这里的333必须要有字段名, 否则无法保存
+            options.setTextContentName(TEXT_NAME);
+
+            // 数组节点名称
+            // <array> <aa>1</aa> <aa>2</aa> </array>
+            // 上例: array下全是同名节点, 将会合并为数组 { aa:[1,2] }
+            // <array> <aa>1</aa> </array>
+            // 上例: array下只有一个节点, 也会合并为数组 { aa:[1] }
+            // <array> <aa>1</aa> <aa>2</aa> <bb>3</bb> 444 </array>
+            // 上例: array下存在不同名的节点, 将会包装为数组 { array:[ {aa:1}, {aa:2}, {bb:3}, 444 ] }
+            // <array> 1 </array>
+            // 上例: array下只有一个文本节点, 也会包装为数组 { array:[1] }
+            options.setArrayNodeName(ARRAY_NAME);
+
+            // NONE: 不处理, <soap:Body>转换为soap:body
+            // REMOTE: 移除, <soap:Body>转换为body
+            // NORMALIZING: 标准化处理, <soap:Body>转换为soapBody
+            // DOLLAR_SEPARATOR: 使用$分隔符, <soap:Body>转换为soap$Body
+            options.setNamespacePrefixStrategy(NamespacePrefixStrategy.NONE);
+
+            return options;
+        }
+
         /** 是否使用驼峰命名 **/
         private boolean useCamelNaming = true;
         /** 是否使用属性容器: 如果为true, 属性都集中放在一个容器中; 如果为false则不加容器, 与子元素放在一起 **/
@@ -419,6 +540,10 @@ public class XmlTools {
         private String textContentName = TEXT_NAME;
         /** 数组节点名称 (不区分大小写) **/
         private String arrayNodeName = ARRAY_NAME;
+        /** 数组节点匹配接口 **/
+        private StringMatcher arrayNodeMatcher;
+        /** 命名空间前缀处理策略 **/
+        private NamespacePrefixStrategy namespacePrefixStrategy = NamespacePrefixStrategy.NONE;
 
         /** 是否使用驼峰命名 **/
         public boolean isUseCamelNaming() {
@@ -426,8 +551,9 @@ public class XmlTools {
         }
 
         /** 是否使用驼峰命名 **/
-        public void setUseCamelNaming(boolean useCamelNaming) {
+        public XmlToJsonOptions setUseCamelNaming(boolean useCamelNaming) {
             this.useCamelNaming = useCamelNaming;
+            return this;
         }
 
         /** 是否使用属性容器: 如果为true, 属性都集中放在一个容器中; 如果为false则不加容器, 与子元素放在一起 **/
@@ -439,8 +565,9 @@ public class XmlTools {
         }
 
         /** 是否使用属性容器: 如果为true, 属性都集中放在一个容器中; 如果为false则不加容器, 与子元素放在一起 **/
-        public void setUseAttrContainer(boolean useAttrContainer) {
+        public XmlToJsonOptions setUseAttrContainer(boolean useAttrContainer) {
             this.useAttrContainer = useAttrContainer;
+            return this;
         }
 
         /** 属性容器名称 **/
@@ -449,8 +576,9 @@ public class XmlTools {
         }
 
         /** 属性容器名称 **/
-        public void setAttrContainerName(String attrContainerName) {
+        public XmlToJsonOptions setAttrContainerName(String attrContainerName) {
             this.attrContainerName = VerifyTools.nvl(attrContainerName, ATTR_NAME);
+            return this;
         }
 
         /** 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称 **/
@@ -461,8 +589,9 @@ public class XmlTools {
         }
 
         /** 文本内容的字段名称, 如果节点有属性, 则节点中的文本内容需要有字段名称 **/
-        public void setTextContentName(String textContentName) {
+        public XmlToJsonOptions setTextContentName(String textContentName) {
             this.textContentName = VerifyTools.nvl(textContentName, TEXT_NAME);
+            return this;
         }
 
         /** 数组节点名称 (不区分大小写) **/
@@ -471,8 +600,26 @@ public class XmlTools {
         }
 
         /** 数组节点名称 (不区分大小写) **/
-        public void setArrayNodeName(String arrayNodeName) {
+        public XmlToJsonOptions setArrayNodeName(String arrayNodeName) {
             this.arrayNodeName = arrayNodeName;
+            return this;
+        }
+
+        /** 数组节点名称 (不区分大小写) **/
+        public XmlToJsonOptions addArrayNodeName(String arrayNodeName) {
+            this.arrayNodeName = StringTools.concat(',', this.arrayNodeName, arrayNodeName);
+            return this;
+        }
+
+        /** 数组节点匹配接口 **/
+        public StringMatcher getArrayNodeMatcher() {
+            return arrayNodeMatcher;
+        }
+
+        /** 数组节点匹配接口 **/
+        public XmlToJsonOptions setArrayNodeMatcher(StringMatcher arrayNodeMatcher) {
+            this.arrayNodeMatcher = arrayNodeMatcher;
+            return this;
         }
 
         /** 判断是不是数组节点名称 (不区分大小写) **/
@@ -483,7 +630,18 @@ public class XmlTools {
                     return true;
                 }
             }
-            return false;
+            return arrayNodeMatcher != null && arrayNodeMatcher.matches(nodeName);
+        }
+
+        /** 命名空间前缀处理策略 **/
+        public NamespacePrefixStrategy getNamespacePrefixStrategy() {
+            return namespacePrefixStrategy;
+        }
+
+        /** 命名空间前缀处理策略 **/
+        public XmlToJsonOptions setNamespacePrefixStrategy(NamespacePrefixStrategy namespacePrefixStrategy) {
+            this.namespacePrefixStrategy = namespacePrefixStrategy;
+            return this;
         }
     }
 
@@ -495,4 +653,16 @@ public class XmlTools {
     /** 静态工具类私有构造方法 **/
     private XmlTools() {
     }
+
+    /** 命名空间前缀处理策略 **/
+    public enum NamespacePrefixStrategy {
+        /** 不处理, <soap:Body>转换为soap:body **/
+        NONE,
+        /** 移除, <soap:Body>转换为body **/
+        REMOVE,
+        /** 标准化处理, <soap:Body>转换为soapBody **/
+        NORMALIZING,
+        /** 使用$分隔符, <soap:Body>转换为soap$Body **/
+        DOLLAR_SEPARATOR
+    }
 }
diff --git a/able/src/main/resources/settings/global/json-service.yml b/able/src/main/resources/settings/global/json-service.yml
new file mode 100644
index 0000000000000000000000000000000000000000..62e63e7ae6d32a8dda49f07ae1fd7171dc187ddf
--- /dev/null
+++ b/able/src/main/resources/settings/global/json-service.yml
@@ -0,0 +1,10 @@
+json-service:
+  fastjson:
+    serviceClass: com.gitee.qdbp.json.JsonServiceForFastjson
+    requiredClass: com.alibaba.fastjson.JSON
+  jackson:
+    serviceClass: com.gitee.qdbp.json.JsonServiceForJackson
+    requiredClass: com.fasterxml.jackson.databind.ObjectMapper
+  gson:
+    serviceClass: com.gitee.qdbp.json.JsonServiceForGson
+    requiredClass: com.google.gson.Gson
diff --git a/able/src/main/resources/settings/i18n/FileErrorCode_zh_CN.properties b/able/src/main/resources/settings/i18n/FileErrorCode_zh_CN.properties
index 10ee81cca51a0459bf8bc38db7c1781749d62c49..253494b85a0b5d4dc96a4eb040f34347ce1fccf6 100644
--- a/able/src/main/resources/settings/i18n/FileErrorCode_zh_CN.properties
+++ b/able/src/main/resources/settings/i18n/FileErrorCode_zh_CN.properties
@@ -1,6 +1,20 @@
 
 FILE_NOT_FOUND = 文件不存在
 FILE_READ_ERROR = 文件读取失败
+FILE_QUERY_ERROR = 文件查询失败
 FILE_WRITE_ERROR = 文件写入失败
-FILE_FORMAT_ERROR = 文件格式错误
+FILE_SAVE_ERROR = 文件保存失败
+FILE_IMPORT_ERROR = 文件导入失败
+FILE_EXPORT_ERROR = 文件导出失败
+FILE_UPLOAD_ERROR = 文件上传失败
+FILE_DOWNLOAD_ERROR = 文件下载失败
+FILE_RENAME_ERROR = 文件重命名失败
+FILE_DELETE_ERROR = 文件删除失败
+FILE_HANDLE_ERROR = 文件处理失败
+
 FILE_TEMPLATE_ERROR = 文件模板格式错误
+FILE_FORMAT_ERROR = 文件格式错误
+FILE_PATH_ERROR = 文件路径错误
+FILE_SIZE_OVER_LIMIT = 文件大小超出限制
+FILE_EXTENSION_LOSE = 缺少文件扩展名
+FILE_EXTENSION_ERROR = 不支持的文件类型
\ No newline at end of file
diff --git a/json/pom.xml b/json/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6ef795c9df2be71eec31f8f75d8f1f6c966fcd97
--- /dev/null
+++ b/json/pom.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-parent</artifactId>
+		<version>5.0.1</version>
+	</parent>
+
+	<artifactId>qdbp-json</artifactId>
+	<packaging>jar</packaging>
+	<version>5.5.15</version>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json library</description>
+
+    <organization>
+        <name>zhaohuihua</name>
+        <url>https://gitee.com/qdbp/qdbp-json/</url>
+    </organization>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-able</artifactId>
+			<version>5.5.15</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>1.2.83</version>
+			<optional>true</optional>
+		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+			<!--<version>2.7.9.7</version>-->
+			<version>2.14.2</version>
+			<optional>true</optional>
+		</dependency>
+		<dependency>
+			<groupId>com.google.code.gson</groupId>
+			<artifactId>gson</artifactId>
+			<version>2.10.1</version>
+			<optional>true</optional>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.7.25</version>
+			<optional>true</optional>
+		</dependency>
+
+	</dependencies>
+
+</project>
diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonInitializer.java b/json/src/main/java/com/gitee/qdbp/json/JsonInitializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fb3d5e29a2079a97d3068f6c2bed3bba7af4fa4
--- /dev/null
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonInitializer.java
@@ -0,0 +1,52 @@
+package com.gitee.qdbp.json;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JSON全局特性初始化<br>
+ * 正确的操作顺序是先调JsonInitializer, 再使用JsonService
+ *
+ * @author zhaohuihua
+ * @version 20230126
+ */
+public class JsonInitializer {
+
+    private static final Logger log = LoggerFactory.getLogger(JsonServiceForFastjson.class);
+
+    private static JsonFeature.Serialization GLOBAL_SERIALIZATION_FEATURE;
+
+    public static void initGlobalSerializationFeature(JsonFeature.Serialization feature) {
+        if (GLOBAL_SERIALIZATION_FEATURE != null && log.isDebugEnabled()) {
+            // 正确的操作顺序是先调JsonInitializer.initGlobalSerializationFeature(), 再使用JsonService
+            log.debug("Json global serialization feature already initialized, " +
+                    "executing again will override the global setting.");
+        }
+        GLOBAL_SERIALIZATION_FEATURE = feature.copy().lockToUnmodifiable();
+    }
+
+    protected static JsonFeature.Serialization getGlobalSerializationFeature() {
+        if (GLOBAL_SERIALIZATION_FEATURE == null) {
+            GLOBAL_SERIALIZATION_FEATURE = new JsonFeature.Serialization().lockToUnmodifiable();
+        }
+        return GLOBAL_SERIALIZATION_FEATURE;
+    }
+
+    private static JsonFeature.Deserialization GLOBAL_DESERIALIZATION_FEATURE;
+
+    public static void initGlobalDeserializationFeature(JsonFeature.Deserialization feature) {
+        if (GLOBAL_DESERIALIZATION_FEATURE != null && log.isDebugEnabled()) {
+            // 正确的操作顺序是先调JsonInitializer.initGlobalDeserializationFeature(), 再使用JsonService
+            log.debug("Json global deserialization feature already initialized, " +
+                    "executing again will override the global setting.");
+        }
+        GLOBAL_DESERIALIZATION_FEATURE = feature.copy().lockToUnmodifiable();
+    }
+
+    protected static JsonFeature.Deserialization getGlobalDeserializationFeature() {
+        if (GLOBAL_DESERIALIZATION_FEATURE == null) {
+            GLOBAL_DESERIALIZATION_FEATURE = new JsonFeature.Deserialization().lockToUnmodifiable();
+        }
+        return GLOBAL_DESERIALIZATION_FEATURE;
+    }
+}
diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6f2f0d49467256e2e2c8fbfd835f2e81fb0cca3
--- /dev/null
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForBase.java
@@ -0,0 +1,170 @@
+package com.gitee.qdbp.json;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+
+/**
+ * JsonService的基础实现类
+ *
+ * @author zhaohuihua
+ * @version 20211207
+ */
+public abstract class JsonServiceForBase implements JsonService {
+
+    protected final JsonFeature.Serialization serializationFeature;
+
+    protected final JsonFeature.Deserialization deserializationFeature;
+
+    public JsonServiceForBase() {
+        this.serializationFeature = JsonInitializer.getGlobalSerializationFeature();
+        this.deserializationFeature = JsonInitializer.getGlobalDeserializationFeature();
+    }
+
+    public JsonServiceForBase(JsonFeature.Serialization serializationFeature,
+            JsonFeature.Deserialization deserializationFeature) {
+        this.serializationFeature = serializationFeature.copy().lockToUnmodifiable();
+        this.deserializationFeature = deserializationFeature.copy().lockToUnmodifiable();
+    }
+
+    @Override
+    public <T> T parseAsObject(String jsonString, Class<T> clazz) {
+        return parseAsObject(jsonString, clazz, deserializationFeature);
+    }
+
+    @Override
+    public String toJsonString(Object object) {
+        return toJsonString(object, serializationFeature);
+    }
+
+    @Override
+    public Map<String, Object> parseAsMap(String jsonString) {
+        return parseAsMap(jsonString, deserializationFeature);
+    }
+
+    @Override
+    public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz) {
+        return parseAsObjects(jsonString, clazz, deserializationFeature);
+    }
+
+    @Override
+    public List<Map<String, Object>> parseAsMaps(String jsonString) {
+        return parseAsMaps(jsonString, deserializationFeature);
+    }
+
+    /**
+     * 将Java对象转换为Map<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param bean JavaBean对象
+     * @return Map
+     */
+    @Override
+    public Map<String, Object> beanToMap(Object bean) {
+        if (bean == null) {
+            return null;
+        }
+        return beanToMap(bean, true, true);
+    }
+
+    /**
+     * 将Map数组转换为Java对象列表
+     *
+     * @param <T> 目标类型
+     * @param maps Map数组
+     * @param clazz 目标Java类
+     * @return Java对象
+     */
+    @Override
+    public <T, V> List<T> mapToBeans(Collection<Map<String, V>> maps, Class<T> clazz) {
+        if (maps == null) {
+            return null;
+        }
+        List<T> result = new ArrayList<>();
+        for (Map<String, ?> map : maps) {
+            result.add(mapToBean(map, clazz));
+        }
+        return result;
+    }
+
+    /**
+     * 将Java对象转换为Map列表<br>
+     * copy from fastjson JSON.toJSON(), 保留enum和date
+     *
+     * @param beans JavaBean对象列表
+     * @return Map列表
+     */
+    @Override
+    public List<Map<String, Object>> beanToMaps(Collection<?> beans) {
+        return beanToMaps(beans, true, true);
+    }
+
+    /**
+     * 将Java对象转换为Map列表
+     *
+     * @param beans Java对象
+     * @param deep 是否递归转换子对象
+     * @param clearBlankValue 是否清除空值
+     * @return Map列表
+     */
+    @Override
+    public List<Map<String, Object>> beanToMaps(Collection<?> beans, boolean deep, boolean clearBlankValue) {
+        if (beans == null) {
+            return null;
+        }
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (Object bean : beans) {
+            result.add(beanToMap(bean, deep, clearBlankValue));
+        }
+        return result;
+    }
+
+    //\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
+    // 以下是低效率的默认实现方法, 如果对应的json框架有更高效的方法, 应重写
+    ///////////////////////////////////////////////////////////////
+
+    @Override
+    public void mapFillToBean(Map<String, ?> map, Object bean) {
+        if (map == null || map.isEmpty()) {
+            return;
+        }
+        Class<?> type = bean.getClass();
+        Object newer = mapToBean(map, type);
+        Field[] fields = ReflectTools.getAllFields(type);
+        for (Field field : fields) {
+            if (map.containsKey(field.getName())) {
+                try {
+                    Object value = ReflectTools.getFieldValue(newer, field);
+                    ReflectTools.setFieldValue(bean, field, value);
+                } catch (Exception ignore) {
+                }
+            }
+        }
+    }
+
+    @Override
+    public <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
+        if (map == null) {
+            return null;
+        }
+        String string = toJsonString(map, serializationFeature);
+        return parseAsObject(string, clazz, deserializationFeature);
+    }
+
+    @Override
+    public Map<String, Object> beanToMap(Object bean, boolean deep, boolean clearBlankValue) {
+        if (bean == null) {
+            return null;
+        }
+        String string = toJsonString(bean, serializationFeature);
+        Map<String, Object> map = parseAsMap(string, deserializationFeature);
+        if (clearBlankValue) {
+            MapTools.clearBlankValue(map);
+        }
+        return map;
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
similarity index 36%
rename from tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
rename to json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
index 5d144b816599b57a7e64060aee9f41bd172223d3..0473bf078225ee1bdbab1a078e9cfd63f68961d3 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/JsonTools.java
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForFastjson.java
@@ -1,12 +1,9 @@
-package com.gitee.qdbp.tools.utils;
+package com.gitee.qdbp.json;
 
 import java.lang.reflect.Array;
-import java.lang.reflect.Field;
-import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -17,10 +14,13 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONException;
 import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.annotation.JSONType;
+import com.alibaba.fastjson.parser.DefaultJSONParser;
+import com.alibaba.fastjson.parser.Feature;
+import com.alibaba.fastjson.parser.JSONLexer;
+import com.alibaba.fastjson.parser.JSONToken;
 import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.parser.deserializer.FieldDeserializer;
 import com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer;
-import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
 import com.alibaba.fastjson.serializer.DoubleSerializer;
 import com.alibaba.fastjson.serializer.JSONSerializable;
 import com.alibaba.fastjson.serializer.JSONSerializer;
@@ -29,292 +29,209 @@ import com.alibaba.fastjson.serializer.ObjectSerializer;
 import com.alibaba.fastjson.serializer.SerializeConfig;
 import com.alibaba.fastjson.serializer.SerializeWriter;
 import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.alibaba.fastjson.util.FieldInfo;
+import com.alibaba.fastjson.serializer.SimpleDateFormatSerializer;
 import com.alibaba.fastjson.util.TypeUtils;
-import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
- * Json工具类
+ * JsonService的Fastjson实现类<br>
+ * 最低支持版本为fastjson-1.2.16
  *
  * @author zhaohuihua
- * @version 180621
+ * @version 20211207
  */
-public class JsonTools {
+public class JsonServiceForFastjson extends JsonServiceForBase {
 
-    /** 静态工具类私有构造方法 **/
-    private JsonTools() {
+    /** 全局实例 **/
+    private static JsonServiceForFastjson DEFAULTS;
+
+    /** 获取全局默认实例 **/
+    public static JsonServiceForFastjson defaults() {
+        if (DEFAULTS == null) {
+            DEFAULTS = InnerInstance.INSTANCE;
+        }
+        return DEFAULTS;
     }
 
-    public static String getMapString(Map<String, Object> map, String key) {
-        Object value = map.get(key);
-        return TypeUtils.castToJavaBean(value, String.class);
+    private static class InnerInstance {
+
+        public static final JsonServiceForFastjson INSTANCE = new JsonServiceForFastjson();
     }
 
-    public static <T> T getMapValue(Map<String, Object> map, String key, Class<T> clazz) {
-        Object value = map.get(key);
-        return TypeUtils.castToJavaBean(value, clazz);
+    public JsonServiceForFastjson() {
+        super();
     }
 
-    public static <T> T getMapValue(Map<String, Object> map, String key, T defaults, Class<T> clazz) {
-        Object value = map.get(key);
-        return TypeUtils.castToJavaBean(value == null ? defaults : value, clazz);
+    public JsonServiceForFastjson(JsonFeature.Serialization serializationFeature,
+            JsonFeature.Deserialization deserializationFeature) {
+        super(serializationFeature, deserializationFeature);
     }
 
-    public static <T> List<T> getMapValues(Map<String, Object> map, String key, Class<T> clazz) {
-        return getMapValues(map, key, false, clazz);
+    @Override
+    public <T> T convert(Object object, Class<T> clazz) {
+        return TypeUtils.castToJavaBean(object, clazz);
     }
 
-    public static <T> List<T> getMapValues(Map<String, Object> map, String key, boolean nullToEmpty, Class<T> clazz) {
-        Object value = map.get(key);
-        if (!(value instanceof Collection)) {
-            return nullToEmpty ? new ArrayList<T>() : null;
+    @Override
+    public String toJsonString(Object object, JsonFeature.Serialization feature) {
+        if (object == null) {
+            return null;
         }
-        Collection<?> collection = (Collection<?>) value;
-        List<T> list = new ArrayList<>();
-        for (Object item : collection) {
-            list.add(TypeUtils.castToJavaBean(item, clazz));
+        try (SerializeWriter out = new SerializeWriter()) {
+            JSONSerializer serializer = generateJsonSerializer(out, feature);
+            serializer.write(object);
+            return out.toString();
         }
-        return list;
     }
 
-    /**
-     * 将对象转换为以换行符分隔的日志文本<br>
-     * 如 newlineLogs(params, operator) 返回 \n\t{paramsJson}\n\t{operatorJson}<br>
-     * 
-     * @param objects 对象
-     * @return 日志文本
-     */
-    public static String newlineLogs(Object... objects) {
-        StringBuilder buffer = new StringBuilder();
-        for (Object object : objects) {
-            buffer.append("\n\t");
-            if (object == null) {
-                buffer.append("null");
-            } else if (object instanceof String) {
-                buffer.append(object);
-            } else {
-                buffer.append(object.getClass().getSimpleName()).append(": ");
-                buffer.append(toLogString(object));
-            }
+    @Override
+    public <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return null;
         }
-        return buffer.toString();
+        Feature[] features = generateParserFeatures(feature);
+        return JSON.parseObject(jsonString, clazz, features);
     }
 
-    private static final SerializeConfig JSON_CONFIG = new SerializeConfig();
-    static {
-        JSON_CONFIG.put(Double.class, new DoubleSerializer("#.##################"));
+    @Override
+    public Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new LinkedHashMap<>();
+        }
+        if (VerifyTools.isJsonObjectString(jsonString)) {
+            Feature[] features = generateParserFeatures(feature);
+            return JSON.parseObject(jsonString, features);
+        } else {
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR)
+                    .setDetails("parameter must be json object string.");
+        }
     }
 
-    /** 转换为日志字符串, json的key不会有引号, 可以稍微简化一下结构 **/
-    public static String toLogString(Object object) {
-        return toLogString(object, new SerializerFeature[0]);
+    @Override
+    public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        return doParseJsonArray(jsonString, clazz, feature);
     }
 
-    /** 转换为日志字符串, json的key不会有引号, 可以稍微简化一下结构 **/
-    public static String toLogString(Object object, SerializerFeature... features) {
-        if (object == null) {
-            return "null";
-        }
-        try (SerializeWriter out = new SerializeWriter()) {
-            JSONSerializer serializer = new JSONSerializer(out, JSON_CONFIG);
-            serializer.config(SerializerFeature.QuoteFieldNames, false);
-            serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
-            serializer.config(SerializerFeature.DisableCircularReferenceDetect, true);
-            if (features != null) {
-                for (SerializerFeature reature : features) {
-                    serializer.config(reature, true);
-                }
+    protected <T> List<T> doParseJsonArray(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        if (!VerifyTools.isJsonArrayString(jsonString)) {
+            throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR)
+                    .setDetails("parameter must be json array string.");
+        }
+
+        Feature[] features = generateParserFeatures(feature);
+        int featureValues = 0;
+        for (Feature f : features) {
+            featureValues = Feature.config(featureValues, f, true);
+        }
+
+        // 只有 JSON.parseObject(String, Class, Feature[])
+        // 没有 JSON.parseArray(String, Class, Feature[])
+        // 参考 JSON.parseArray(String, Class, ParserConfig)
+        ParserConfig parserConfig = ParserConfig.getGlobalInstance();
+        try (DefaultJSONParser parser = new DefaultJSONParser(jsonString, parserConfig, featureValues)) {
+            JSONLexer lexer = parser.lexer;
+            int token = lexer.token();
+            if (token == JSONToken.NULL) {
+                lexer.nextToken();
+                return new ArrayList<>();
+            } else if (token == JSONToken.EOF && lexer.isBlankInput()) {
+                return new ArrayList<>();
+            } else {
+                List<T> list = new ArrayList<>();
+                parser.parseArray(clazz, list);
+                parser.handleResovleTask(list);
+                return list;
             }
-            serializer.write(object);
-            return out.toString();
         }
     }
 
-    /** 对象转换为字符串 **/
-    public static String toJsonString(Object object) {
-        return toJsonString(object, new SerializerFeature[0]);
-    }
-
-    /** 对象转换为字符串 **/
-    public static String toJsonString(Object object, SerializerFeature... features) {
-        if (object == null) {
-            return "null";
-        }
-        try (SerializeWriter out = new SerializeWriter()) {
-            JSONSerializer serializer = new JSONSerializer(out, JSON_CONFIG);
-            serializer.config(SerializerFeature.QuoteFieldNames, true);
-            serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
-            serializer.config(SerializerFeature.DisableCircularReferenceDetect, true);
-            if (features != null) {
-                for (SerializerFeature reature : features) {
-                    serializer.config(reature, true);
-                }
+    @Override
+    public List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature) {
+        List<Object> objects = parseAsObjects(jsonString, Object.class, feature);
+        if (objects.isEmpty()) {
+            return new ArrayList<>();
+        }
+        List<Map<String, Object>> maps = new ArrayList<>();
+        for (int i = 0; i < objects.size(); i++) {
+            Object item = objects.get(i);
+            if (item instanceof Map) {
+                maps.add(MapTools.toJsonMap((Map<?, ?>) item));
+            } else {
+                throw new ServiceException(ResultCode.PARAMETER_FORMAT_ERROR)
+                        .setDetails("parameter[" + i + "] can not convert to Map.");
             }
-            serializer.write(object);
-            return out.toString();
         }
+        return maps;
     }
 
     /**
-     * 将Map内容设置到Java对象中<br>
-     * copy from JavaBeanDeserializer.createInstance(Map<String, Object>, ParserConfig);
-     * 
+     * 将Map内容设置到Java对象中
+     *
      * @param map Map
      * @param bean 目标Java对象
      */
-    public static void mapFillToBean(Map<String, ?> map, Object bean) {
-        Class<?> clazz = bean.getClass();
-        ParserConfig config = ParserConfig.getGlobalInstance();
-        ObjectDeserializer deserializer = config.getDeserializer(clazz);
-        if (!(deserializer instanceof JavaBeanDeserializer)) {
-            throw new JSONException("can not get javaBeanDeserializer. " + clazz.getName());
+    @Override
+    public void mapFillToBean(Map<String, ?> map, Object bean) {
+        if (map == null || map.isEmpty()) {
+            return;
         }
-        JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer) deserializer;
+        ParserConfig config = ParserConfig.getGlobalInstance();
         try {
-            mapFillToBean(map, bean, config, javaBeanDeser);
+            MapFillToBeanHandler handler = new MapFillToBeanHandler(bean, config);
+            handler.createInstance(MapTools.toJsonMap(map), config);
+        } catch (JSONException e) {
+            throw e;
         } catch (Exception e) {
             throw new JSONException(e.getMessage(), e);
         }
     }
 
-    private static void mapFillToBean(Map<String, ?> map, Object bean, ParserConfig config,
-            JavaBeanDeserializer javaBeanDeser) throws IllegalArgumentException, IllegalAccessException {
-        for (Map.Entry<String, ?> entry : map.entrySet()) {
-            String key = entry.getKey();
-            Object value = entry.getValue();
-
-            FieldDeserializer fieldDeser = javaBeanDeser.smartMatch(key);
-            if (fieldDeser == null) {
-                continue;
-            }
-
-            final FieldInfo fieldInfo = fieldDeser.fieldInfo;
-            Field field = fieldDeser.fieldInfo.field;
-            Type paramType = fieldInfo.fieldType;
-
-            if (field != null) {
-                Class<?> fieldType = field.getType();
-                if (fieldType == boolean.class) {
-                    if (value == Boolean.FALSE) {
-                        field.setBoolean(bean, false);
-                        continue;
-                    }
-
-                    if (value == Boolean.TRUE) {
-                        field.setBoolean(bean, true);
-                        continue;
-                    }
-                } else if (fieldType == int.class) {
-                    if (value instanceof Number) {
-                        field.setInt(bean, ((Number) value).intValue());
-                        continue;
-                    }
-                } else if (fieldType == long.class) {
-                    if (value instanceof Number) {
-                        field.setLong(bean, ((Number) value).longValue());
-                        continue;
-                    }
-                } else if (fieldType == float.class) {
-                    if (value instanceof Number) {
-                        field.setFloat(bean, ((Number) value).floatValue());
-                        continue;
-                    } else if (value instanceof String) {
-                        String strVal = (String) value;
-                        float floatValue;
-                        if (strVal.length() <= 10) {
-                            floatValue = TypeUtils.parseFloat(strVal);
-                        } else {
-                            floatValue = Float.parseFloat(strVal);
-                        }
-
-                        field.setFloat(bean, floatValue);
-                        continue;
-                    }
-                } else if (fieldType == double.class) {
-                    if (value instanceof Number) {
-                        field.setDouble(bean, ((Number) value).doubleValue());
-                        continue;
-                    } else if (value instanceof String) {
-                        String strVal = (String) value;
-                        double doubleValue;
-                        if (strVal.length() <= 10) {
-                            doubleValue = TypeUtils.parseDouble(strVal);
-                        } else {
-                            doubleValue = Double.parseDouble(strVal);
-                        }
-
-                        field.setDouble(bean, doubleValue);
-                        continue;
-                    }
-                } else if (value != null && paramType == value.getClass()) {
-                    field.set(bean, value);
-                    continue;
-                }
-            }
-
-            String format = fieldInfo.format;
-            if (format != null && paramType == Date.class) {
-                value = TypeUtils.castToDate(value, format);
-            } else {
-                if (paramType instanceof ParameterizedType) {
-                    value = TypeUtils.cast(value, (ParameterizedType) paramType, config);
-                } else {
-                    value = TypeUtils.cast(value, paramType, config);
-                }
-            }
-
-            fieldDeser.setValue(bean, value);
-        }
-    }
-
     /**
      * 将Map转换为Java对象
-     * 
+     *
      * @param <T> 目标类型
      * @param map Map
      * @param clazz 目标Java类
      * @return Java对象
      */
-    public static <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
-        @SuppressWarnings("unchecked")
-        Map<String, Object> json = (Map<String, Object>) map;
+    @Override
+    public <T> T mapToBean(Map<String, ?> map, Class<T> clazz) {
+        if (map == null) {
+            return null;
+        }
+        Map<String, Object> json = MapTools.toJsonMap(map);
         return TypeUtils.castToJavaBean(json, clazz, ParserConfig.getGlobalInstance());
     }
 
     /**
      * 将Java对象转换为Map<br>
      * copy from fastjson JSON.toJSON(), 保留enum和date
-     * 
-     * @param bean JavaBean对象
-     * @return Map
-     */
-    public static Map<String, Object> beanToMap(Object bean) {
-        if (bean == null) {
-            return null;
-        }
-        return beanToMap(bean, true, true);
-    }
-
-    /**
-     * 将Java对象转换为Map
-     * 
-     * @param object Java对象
+     *
+     * @param bean Java对象
      * @param deep 是否递归转换子对象
      * @param clearBlankValue 是否清除空值
      * @return Map
      */
-    public static Map<String, Object> beanToMap(Object object, boolean deep, boolean clearBlankValue) {
-        if (object == null) {
+    @Override
+    public Map<String, Object> beanToMap(Object bean, boolean deep, boolean clearBlankValue) {
+        if (bean == null) {
             return null;
         }
-        Map<String, Object> map = getBeanFieldValuesMap(object, deep, SerializeConfig.getGlobalInstance());
+        Map<String, Object> map = getBeanFieldValuesMap(bean, deep, SerializeConfig.getGlobalInstance());
         if (clearBlankValue) {
             MapTools.clearBlankValue(map);
         }
         return map;
     }
 
-    protected static JSONObject getBeanFieldValuesMap(Object bean, boolean deep, SerializeConfig config) {
+    protected JSONObject getBeanFieldValuesMap(Object bean, boolean deep, SerializeConfig config) {
         if (bean == null) {
             return null;
         }
@@ -398,31 +315,42 @@ public class JsonTools {
         throw new IllegalArgumentException(bean.getClass().getSimpleName() + " can't convert to map.");
     }
 
-    private static Object beanToJson(Object bean, SerializeConfig config) {
-        if (bean == null) {
+    /**
+     * 将JavaBean对象转换为JSON对象<br>
+     * 复制代码是为了保留日期/枚举不要被序列化
+     *
+     * @param javaObject JavaBean对象
+     * @param config 序列化配置
+     * @return JSON对象
+     * @see JSON#toJSON(Object, SerializeConfig)
+     */
+    private Object beanToJson(Object javaObject, SerializeConfig config) {
+        if (javaObject == null) {
             return null;
         }
 
-        if (bean instanceof JSON) {
-            return bean;
+        if (javaObject instanceof JSON) {
+            return javaObject;
         }
 
-        if (bean instanceof Map) {
-            Map<?, ?> map = (Map<?, ?>) bean;
+        if (javaObject instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) javaObject;
 
+            int size = map.size();
             Map<String, Object> innerMap;
             if (map instanceof LinkedHashMap) {
-                innerMap = new LinkedHashMap<>(map.size());
+                innerMap = new LinkedHashMap<>(size);
             } else if (map instanceof TreeMap) {
                 innerMap = new TreeMap<>();
             } else {
-                innerMap = new HashMap<>(map.size());
+                innerMap = new HashMap<>(size);
             }
 
             JSONObject json = new JSONObject(innerMap);
 
             for (Map.Entry<?, ?> entry : map.entrySet()) {
-                String jsonKey = TypeUtils.castToString(entry.getKey());
+                Object key = entry.getKey();
+                String jsonKey = TypeUtils.castToString(key);
                 Object jsonValue = beanToJson(entry.getValue(), config);
                 json.put(jsonKey, jsonValue);
             }
@@ -430,8 +358,8 @@ public class JsonTools {
             return json;
         }
 
-        if (bean instanceof Collection) {
-            Collection<?> collection = (Collection<?>) bean;
+        if (javaObject instanceof Collection) {
+            Collection<?> collection = (Collection<?>) javaObject;
 
             JSONArray array = new JSONArray(collection.size());
 
@@ -443,34 +371,31 @@ public class JsonTools {
             return array;
         }
 
-        if (bean instanceof JSONSerializable) {
-            String json = JSON.toJSONString(bean);
+        if (javaObject instanceof JSONSerializable) {
+            String json = JSON.toJSONString(javaObject);
             return JSON.parse(json);
         }
 
-        Class<?> clazz = bean.getClass();
+        Class<?> clazz = javaObject.getClass();
 
         if (clazz.isEnum()) {
             // return ((Enum<?>) bean).name();
-            return bean;
+            return javaObject;
         }
         if (clazz == String.class) {
-            return bean;
+            return javaObject;
         }
         if (CharSequence.class.isAssignableFrom(clazz)) {
-            return bean.toString();
-        }
-        if (ReflectTools.isPrimitive(clazz, false)) {
-            return bean; // 保留原始类型及原始类型的包装类型/字符串/日期/枚举/Number的所有子类
+            return javaObject.toString();
         }
 
         if (clazz.isArray()) {
-            int len = Array.getLength(bean);
+            int len = Array.getLength(javaObject);
 
             JSONArray array = new JSONArray(len);
 
             for (int i = 0; i < len; ++i) {
-                Object item = Array.get(bean, i);
+                Object item = Array.get(javaObject, i);
                 Object jsonValue = beanToJson(item, config);
                 array.add(jsonValue);
             }
@@ -478,13 +403,33 @@ public class JsonTools {
             return array;
         }
 
+        if (ReflectTools.isPrimitive(clazz, false)) {
+            // 保留原始类型及原始类型的包装类型/字符串/日期/枚举/Number的所有子类
+            return javaObject;
+        }
+
         ObjectSerializer serializer = config.getObjectWriter(clazz);
         if (serializer instanceof JavaBeanSerializer) {
             JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer;
 
-            JSONObject json = new JSONObject();
+            boolean ordered = false;
+            if (serializerHasGetJsonType) {
+                JSONType jsonType = javaBeanSerializer.getJSONType();
+                if (jsonType != null) {
+                    for (SerializerFeature serializerFeature : jsonType.serialzeFeatures()) {
+                        if (serializerFeature == SerializerFeature.SortField
+                                || serializerFeature == SerializerFeature.MapSortField) {
+                            ordered = true;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            JSONObject json = new JSONObject(ordered);
             try {
-                Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(bean);
+                // 自fastjson-1.2.11开始, 有getFieldValuesMap()方法
+                Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
                 for (Map.Entry<String, Object> entry : values.entrySet()) {
                     json.put(entry.getKey(), beanToJson(entry.getValue(), config));
                 }
@@ -494,74 +439,133 @@ public class JsonTools {
             return json;
         }
 
-        String text = JSON.toJSONString(bean);
+        String text = JSON.toJSONString(javaObject);
         return JSON.parse(text);
     }
 
-    /**
-     * 将字符串转换为KeyString对象<br>
-     * toKeyString("{'key':1,'value':'冷水'}")<br>
-     * 或: toKeyString("{'1':'冷水'}")<br>
-     *
-     * @param text 字符串
-     * @return KeyString对象
-     */
-    public static KeyString toKeyString(String text) {
-        if (VerifyTools.isBlank(text)) {
-            return null;
+
+    protected static JSONSerializer generateJsonSerializer(SerializeWriter writer, JsonFeature.Serialization feature) {
+        SerializeConfig config = new SerializeConfig();
+        if (!feature.isWriteDateAsTimestamp() && feature.getDateFormatPattern() != null) {
+            config.put(Date.class, new SimpleDateFormatSerializer(feature.getDateFormatPattern()));
+        }
+        if (feature.getDoubleFormatPattern() != null) {
+            config.put(Double.class, new DoubleSerializer(feature.getDoubleFormatPattern()));
+        }
+        JSONSerializer serializer = new JSONSerializer(writer, config);
+        // fastjson无法按对象的原始字段顺序输出, 设置为true是按字母顺序, 设置为false则是乱序
+        // serializer.config(SerializerFeature.SortField, false);
+        // 是否处理循环引用 (设置为true就不会报错)
+        serializer.config(SerializerFeature.DisableCircularReferenceDetect, !feature.isHandleCircularReference());
+        // 字段名是否使用引号括起来
+        serializer.config(SerializerFeature.QuoteFieldNames, feature.isQuoteFieldNames());
+        // 是否使用单引号
+        serializer.config(SerializerFeature.UseSingleQuotes, feature.isUseSingleQuotes());
+        // 是否输出值为null的字段
+        // serializer.config(SerializerFeature.WriteFieldNullValue, feature.isWriteFieldNullValue());
+        // 是否输出Map中的null值
+        serializer.config(SerializerFeature.WriteMapNullValue, feature.isWriteMapNullValue());
+        // 输出枚举时是否使用toString
+        serializer.config(SerializerFeature.WriteEnumUsingToString, feature.isWriteEnumUsingToString());
+        // 序列化List类型数据时是否将null输出为空数组
+        serializer.config(SerializerFeature.WriteNullListAsEmpty, feature.isWriteNullListAsEmpty());
+        // 序列化String类型数据时是否将null输出为空字符串
+        serializer.config(SerializerFeature.WriteNullStringAsEmpty, feature.isWriteNullStringAsEmpty());
+        // 序列化Number类型数据时是否将null输出为0
+        serializer.config(SerializerFeature.WriteNullNumberAsZero, feature.isWriteNullNumberAsZero());
+        // 序列化Boolean类型数据时是否将null输出为false
+        serializer.config(SerializerFeature.WriteNullBooleanAsFalse, feature.isWriteNullBooleanAsFalse());
+        // 序列化BigDecimal类型数据时是否使用toPlainString()
+        // Since: 1.2.16
+        serializer.config(SerializerFeature.WriteBigDecimalAsPlain, feature.isWriteBigDecimalAsPlain());
+        // 是否跳过transient修饰的字段
+        serializer.config(SerializerFeature.SkipTransientField, feature.isSkipTransientField());
+        // 是否输出格式化后的Json字符串
+        serializer.config(SerializerFeature.PrettyFormat, feature.isPrettyFormat());
+        // 序列化Date类型数据时是否输出为long时间戳
+        if (feature.isWriteDateAsTimestamp()) {
+            serializer.config(SerializerFeature.WriteDateUseDateFormat, false);
+        } else {
+            serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
+            if (feature.getDateFormatPattern() != null) {
+                serializer.setDateFormat(feature.getDateFormatPattern());
+            }
         }
-        JSONObject json = JSON.parseObject(text);
-        return toKeyString(json);
+        return serializer;
     }
 
-    /**
-     * 将字符串转换为KeyString对象数组<br>
-     * toKeyStrings("[{'key':1,'value':'冷水'},{'key':2,'value':'热水'}]")<br>
-     * 或: toKeyStrings("{'1':'冷水','2':'热水','3':'直饮水'}")<br>
-     * --&gt; List&lt;KeyString&gt;
-     *
-     * @param text 字符串
-     * @return List&lt;KeyString&gt; 对象数组
-     */
-    public static List<KeyString> toKeyStrings(String text) {
-        if (VerifyTools.isBlank(text)) {
-            return null;
-        }
-        List<KeyString> list = new ArrayList<>();
+    protected static Feature[] generateParserFeatures(JsonFeature.Deserialization feature) {
+        List<Feature> config = new ArrayList<>();
+        // Since: 1.2.42
+        if (supportNonStringKeyAsString) {
+            config.add(Feature.NonStringKeyAsString);
+        }
+        // 是否自动关闭流
+        config.add(Feature.AutoCloseSource);
+        // 对字段名调用String.intern()
+        config.add(Feature.InternFieldNames);
+        // 是否允许Json字符串中带注释
+        config.add(Feature.AllowComment);
+        // 是否允许json字段名不被引号括起来
+        config.add(Feature.AllowUnQuotedFieldNames);
+        // 是否允许json字段名和字段值使用单引号括起来
+        config.add(Feature.AllowSingleQuotes);
+        // 是否允许多余的逗号
+        config.add(Feature.AllowArbitraryCommas);
+        // 排序的字段顺序快速匹配
+        config.add(Feature.SortFeidFastMatch);
+        // 忽略不匹配
+        config.add(Feature.IgnoreNotMatch);
+        // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
+        if (feature.isUseBigDecimalForFloats()) {
+            config.add(Feature.UseBigDecimal);
+        }
+        // 遇到未知属性时是否抛出异常 ( FastJson不支持, 默认不会抛出异常 )
+        // if (feature.isFailOnUnknownFields()) {
+        // }
+        // 遇到未知的枚举值时是否抛出异常
+        if (feature.isFailOnUnknownEnumValues() && supportErrorOnEnumNotMatch) {
+            // Since: 1.2.55
+            config.add(Feature.ErrorOnEnumNotMatch);
+        }
+        // 是否将null字段串初始化为空字符串
+        if (feature.isReadNullStringAsEmpty()) {
+            config.add(Feature.InitStringFieldAsEmpty);
+        }
+        return config.toArray(new Feature[0]);
+    }
 
-        Object object = JSON.parse(text);
-        if (object instanceof JSONArray) {
-            JSONArray array = (JSONArray) object;
-            for (Object i : array) {
-                list.add(toKeyString((JSONObject) i));
-            }
-        } else if (object instanceof JSONObject) {
-            JSONObject json = (JSONObject) object;
-            for (Map.Entry<String, Object> entry : json.entrySet()) {
-                String key = entry.getKey();
-                Object value = entry.getValue();
-                String string = TypeUtils.castToJavaBean(value, String.class);
-                list.add(new KeyString(key, string));
-            }
-        }
+    private static boolean existField(Class<?> clazz, String fieldName) {
+        return ReflectTools.findField(clazz, fieldName) != null;
+    }
 
-        Collections.sort(list);
-        return list;
+    // Since: 1.2.42
+    private static final boolean supportErrorOnEnumNotMatch;
+    // Since: 1.2.55
+    private static final boolean supportNonStringKeyAsString;
+    // Since: 1.2.76
+    private static final boolean serializerHasGetJsonType;
 
+    static {
+        supportErrorOnEnumNotMatch = existField(Feature.class, "ErrorOnEnumNotMatch");
+        supportNonStringKeyAsString = existField(Feature.class, "NonStringKeyAsString");
+        serializerHasGetJsonType = existField(JavaBeanSerializer.class, "getJSONType");
     }
 
-    private static KeyString toKeyString(JSONObject json) {
-        if (json.containsKey("key")) {
-            return JSON.toJavaObject(json, KeyString.class);
-        } else {
-            for (Map.Entry<String, Object> entry : json.entrySet()) {
-                String key = entry.getKey();
-                Object value = entry.getValue();
-                String string = TypeUtils.castToJavaBean(value, String.class);
-                return new KeyString(key, string);
-            }
-            throw new IllegalArgumentException("json is empty.");
+    private static class MapFillToBeanHandler extends JavaBeanDeserializer {
+        private final Object bean;
+
+        public MapFillToBeanHandler(Object bean, ParserConfig config) {
+            super(config, bean.getClass());
+            this.bean = bean;
         }
-    }
 
+        @Override
+        public Object createInstance(DefaultJSONParser parser, Type type) {
+            // 调用此方法时: JavaBeanDeserializer.createInstance(Map<String, Object> map, ParserConfig config)
+            // 会调用本方法: createInstance(DefaultJSONParser parser, Type type)
+            // 这里直接返回构造方法中传入的bean, 达到将map赋值给已知对象的目的
+            return bean;
+        }
+    }
 }
diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForGson.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForGson.java
new file mode 100644
index 0000000000000000000000000000000000000000..0ef7b30e06723dc1ef02133c694128058f029045
--- /dev/null
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForGson.java
@@ -0,0 +1,178 @@
+package com.gitee.qdbp.json;
+
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.ToNumberPolicy;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * JsonService的Gson实现类
+ *
+ * @author zhaohuihua
+ * @version 20211207
+ */
+public class JsonServiceForGson extends JsonServiceForBase {
+
+    @Override
+    public <T> T convert(Object object, Class<T> clazz) {
+        String string = toJsonString(object, serializationFeature);
+        return parseAsObject(string, clazz, deserializationFeature);
+    }
+
+    @Override
+    public String toJsonString(Object object, JsonFeature.Serialization feature) {
+        if (object == null) {
+            return null;
+        }
+        Gson gson = generateGson(feature, deserializationFeature);
+        return gson.toJson(object);
+    }
+
+    @Override
+    public Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new LinkedHashMap<>();
+        }
+        Gson gson = generateGson(serializationFeature, feature);
+        Type type = new TypeToken<Map<String, Object>>() {}.getType();
+        return gson.fromJson(jsonString, type);
+    }
+
+    @Override
+    public List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        Gson gson = generateGson(serializationFeature, feature);
+        Type type = new TypeToken<List<Map<String, Object>>>() {}.getType();
+        return gson.fromJson(jsonString, type);
+    }
+
+    @Override
+    public <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return null;
+        }
+        Gson gson = generateGson(serializationFeature, feature);
+        return gson.fromJson(jsonString, clazz);
+    }
+
+    @Override
+    public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        Gson gson = generateGson(serializationFeature, feature);
+        Type type = TypeToken.getParameterized(List.class, clazz).getType();
+        return gson.fromJson(jsonString, type);
+    }
+
+    protected Gson generateGson(JsonFeature.Serialization sf, JsonFeature.Deserialization df) {
+        GsonBuilder builder = new GsonBuilder();
+        // 禁用HTML转义
+        builder.disableHtmlEscaping();
+        // 宽容模式
+        builder.setLenient();
+        // 默认的转数字策略是ToNumberPolicy.DOUBLE, 导致所有的整数全转换为double了
+        builder.setObjectToNumberStrategy(ToNumberPolicy.LAZILY_PARSED_NUMBER);
+
+        // 是否处理循环引用 (设置为true时不应该报错)
+        // mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
+        // mapper.configure(SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL, sf.isHandleCircularReference());
+        // 字段名是否使用引号括起来
+        // mapper.configure(JsonWriteFeature.QUOTE_FIELD_NAMES, feature.isQuoteFieldNames());
+        // 是否使用单引号 (jackson不支持)
+        // feature.isUseSingleQuotes()
+        // 是否输出值为null的字段
+        if (sf.isWriteMapNullValue()) {
+            builder.serializeNulls();
+        }
+        // 是否输出Map中的null值
+        // sf.isWriteMapNullValue()
+        // 输出枚举时是否使用toString
+        // mapper.configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, !sf.isWriteEnumUsingToString());
+        // mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, sf.isWriteEnumUsingToString());
+
+        // gson无法实现null转换为其他值, 因为TreeTypeAdapter的nullSafe=true
+        // 只有注解才通指定@JsonAdapter(nullSafe=false)
+        // 序列化List类型数据时是否将null输出为空数组
+        if (sf.isWriteNullListAsEmpty()) {
+        }
+        // 序列化String类型数据时是否将null输出为空字符串
+        if (sf.isWriteNullStringAsEmpty()) {
+            // builder.registerTypeAdapter(String.class, new StringNullValueSerializer());
+        }
+        // 序列化Number类型数据时是否将null输出为0
+        if (sf.isWriteNullNumberAsZero()) {
+            // builder.registerTypeAdapter(Number.class, new NumberNullValueSerializer());
+        }
+        // 序列化Boolean类型数据时是否将null输出为false
+        if (sf.isWriteNullBooleanAsFalse()) {
+            // builder.registerTypeAdapter(Boolean.class, new BooleanNullValueSerializer());
+        }
+
+        // 序列化BigDecimal类型数据时是否使用toPlainString()
+        if (sf.isWriteBigDecimalAsPlain()) {
+            builder.registerTypeAdapter(BigDecimal.class, new BigDecimalToPlainStringSerializer());
+        }
+        // 是否跳过transient修饰的字段
+        if (sf.isSkipTransientField()) {
+            builder.excludeFieldsWithModifiers(Modifier.TRANSIENT);
+        }
+        // 是否输出格式化后的Json字符串
+        if (sf.isPrettyFormat()) {
+            builder.setPrettyPrinting();
+        }
+        // 序列化Date类型数据时是否输出为long时间戳
+        if (sf.isWriteDateAsTimestamp()) {
+            builder.registerTypeAdapter(Date.class, new DateToTimestampSerializer());
+        }
+        if (!sf.isWriteDateAsTimestamp() && sf.getDateFormatPattern() != null) {
+            builder.setDateFormat(sf.getDateFormatPattern());
+        }
+
+        // // 是否允许Json字符串中带注释
+        // mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
+        // // 是否允许json字段名不被引号括起来
+        // mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
+        // // 是否允许json字段名和字段值使用单引号括起来
+        // mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
+        // // 遇到未知属性时是否抛出异常
+        // mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, df.isFailOnUnknownFields());
+        // // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
+        // mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, df.isUseBigDecimalForFloats());
+        // // 遇到未知属性时是否抛出异常
+        // mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, !df.isFailOnUnknownEnumValues());
+        // // 是否将null字段串初始化为空字符串
+        // // feature.isReadNullStringAsEmpty()
+
+        return builder.create();
+    }
+
+    private static class BigDecimalToPlainStringSerializer implements JsonSerializer<BigDecimal> {
+        @Override
+        public JsonElement serialize(BigDecimal src, Type typeOfSrc, JsonSerializationContext context) {
+            return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.toPlainString());
+        }
+    }
+
+    private static class DateToTimestampSerializer implements JsonSerializer<Date> {
+        @Override
+        public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
+            return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.getTime());
+        }
+    }
+}
diff --git a/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java
new file mode 100644
index 0000000000000000000000000000000000000000..dafb4b31d907271bc99b3c7ec995a4734d1dbd29
--- /dev/null
+++ b/json/src/main/java/com/gitee/qdbp/json/JsonServiceForJackson.java
@@ -0,0 +1,198 @@
+package com.gitee.qdbp.json;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+
+/**
+ * JsonService的Jackson实现类
+ *
+ * @author zhaohuihua
+ * @version 20211207
+ */
+public class JsonServiceForJackson extends JsonServiceForBase {
+
+    @Override
+    public <T> T convert(Object object, Class<T> clazz) {
+        String string = toJsonString(object, serializationFeature);
+        return parseAsObject(string, clazz, deserializationFeature);
+    }
+
+    @Override
+    public String toJsonString(Object object, JsonFeature.Serialization feature) {
+        if (object == null) {
+            return null;
+        }
+        ObjectMapper mapper = generateObjectMapper(feature, deserializationFeature);
+        try {
+            return mapper.writeValueAsString(object);
+        } catch (IOException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e)
+                    .setDetails("{} to json string error", object.getClass().getSimpleName());
+        }
+    }
+
+    @Override
+    public Map<String, Object> parseAsMap(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new LinkedHashMap<>();
+        }
+        ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
+        try {
+            return mapper.readValue(jsonString, new TypeReference<Map<String, Object>>() {});
+        } catch (IOException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json object string error");
+        }
+    }
+
+    @Override
+    public List<Map<String, Object>> parseAsMaps(String jsonString, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
+        try {
+            return mapper.readValue(jsonString, new TypeReference<List<Map<String, Object>>>() {});
+        } catch (IOException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json array string error");
+        }
+    }
+
+    @Override
+    public <T> T parseAsObject(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return null;
+        }
+        ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
+        try {
+            return mapper.readValue(jsonString, clazz);
+        } catch (IOException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json object string error");
+        }
+    }
+
+    @Override
+    public <T> List<T> parseAsObjects(String jsonString, Class<T> clazz, JsonFeature.Deserialization feature) {
+        if (jsonString == null || jsonString.trim().length() == 0) {
+            return new ArrayList<>();
+        }
+        ObjectMapper mapper = generateObjectMapper(serializationFeature, feature);
+        JavaType targetType = mapper.getTypeFactory().constructCollectionType(List.class, clazz);
+        try {
+            return mapper.readValue(jsonString, targetType);
+        } catch (IOException e) {
+            throw new ServiceException(ResultCode.SERVER_INNER_ERROR, e).setDetails("parse json array string error");
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    protected ObjectMapper generateObjectMapper(JsonFeature.Serialization sf, JsonFeature.Deserialization df) {
+        ObjectMapper mapper = new ObjectMapper();
+        // 是否处理循环引用 (设置为true时不应该报错)
+        // Since: 2.4
+        if (supportFailOnSelfReferences) {
+            mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
+        }
+        // Since: 2.11
+        if (supportWriteSelfReferencesAsNull) {
+            mapper.configure(SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL, sf.isHandleCircularReference());
+        }
+        // 字段名是否使用引号括起来
+        // mapper.configure(JsonWriteFeature.QUOTE_FIELD_NAMES, feature.isQuoteFieldNames());
+        // 是否使用单引号 (jackson不支持)
+        // feature.isUseSingleQuotes()
+        // 是否输出值为null的字段
+        // sf.isWriteFieldNullValue()
+        // 是否输出Map中的null值
+        mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, sf.isWriteMapNullValue());
+        // 输出枚举时是否使用toString
+        mapper.configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, !sf.isWriteEnumUsingToString());
+        mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, sf.isWriteEnumUsingToString());
+        // 序列化List类型数据时是否将null输出为空数组
+        if (sf.isWriteNullListAsEmpty()) {
+
+        }
+        // 序列化String类型数据时是否将null输出为空字符串
+        if (sf.isWriteNullStringAsEmpty()) {
+
+        }
+        // 序列化Number类型数据时是否将null输出为0
+        if (sf.isWriteNullNumberAsZero()) {
+
+        }
+        // 序列化Boolean类型数据时是否将null输出为false
+        if (sf.isWriteNullBooleanAsFalse()) {
+
+        }
+        // 序列化BigDecimal类型数据时是否使用toPlainString()
+        // Since: 2.3
+        if (supportWriteBigDecimalAsPlain) {
+            mapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, sf.isWriteBigDecimalAsPlain());
+        }
+        // 是否跳过transient修饰的字段
+        if (sf.isSkipTransientField()) {
+
+        }
+        // 是否输出格式化后的Json字符串
+        mapper.configure(SerializationFeature.INDENT_OUTPUT, sf.isPrettyFormat());
+        // 序列化Date类型数据时是否输出为long时间戳
+        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, sf.isWriteDateAsTimestamp());
+        if (!sf.isWriteDateAsTimestamp() && sf.getDateFormatPattern() != null) {
+            mapper.setDateFormat(new SimpleDateFormat(sf.getDateFormatPattern()));
+        }
+
+
+        // 是否允许Json字符串中带注释
+        mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
+        // 是否允许json字段名不被引号括起来
+        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
+        // 是否允许json字段名和字段值使用单引号括起来
+        mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
+        // 遇到未知属性时是否抛出异常
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, df.isFailOnUnknownFields());
+        // 是否将浮点数解析为BigDecimal对象, 关闭后解析为Double对象
+        mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, df.isUseBigDecimalForFloats());
+        // 遇到未知属性时是否抛出异常
+        // Since: 2.0
+        if (supportReadUnknownEnumValuesAsNull) {
+            mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, !df.isFailOnUnknownEnumValues());
+        }
+        // 是否将null字段串初始化为空字符串
+        // feature.isReadNullStringAsEmpty()
+
+        return mapper;
+    }
+
+    private static boolean existField(Class<?> clazz, String fieldName) {
+        return ReflectTools.findField(clazz, fieldName) != null;
+    }
+
+    // Since: 2.4
+    private static final boolean supportFailOnSelfReferences;
+    // Since: 2.11
+    private static final boolean supportWriteSelfReferencesAsNull;
+    // Since: 2.3
+    private static final boolean supportWriteBigDecimalAsPlain;
+    // Since: 2.0
+    private static final boolean supportReadUnknownEnumValuesAsNull;
+
+    static {
+        supportFailOnSelfReferences = existField(SerializationFeature.class, "FAIL_ON_SELF_REFERENCES");
+        supportWriteSelfReferencesAsNull = existField(SerializationFeature.class, "WRITE_SELF_REFERENCES_AS_NULL");
+        supportWriteBigDecimalAsPlain = existField(JsonGenerator.Feature.class, "WRITE_BIGDECIMAL_AS_PLAIN");
+        supportReadUnknownEnumValuesAsNull = existField(DeserializationFeature.class, "READ_UNKNOWN_ENUM_VALUES_AS_NULL");
+    }
+}
diff --git a/pom.xml b/pom.xml
index c4f80330cdd6405dd1fadab4fce82256b46261a1..55de6792dceb084abbecc5ea7a10c228271c34ed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
 	<groupId>com.gitee.qdbp</groupId>
 	<artifactId>qdbp-parent</artifactId>
-	<version>5.0.1</version>
+	<version>5.0.2</version>
 	<packaging>pom</packaging>
 	<name>${project.artifactId}</name>
 	<url>https://gitee.com/qdbp/</url>
@@ -29,21 +29,24 @@
 
 	<modules>
 		<module>able</module>
+		<module>json</module>
 		<module>tools</module>
+		<module>test</module>
 	</modules>
 
 	<properties>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<project.build.locales>zh_CN</project.build.locales>
-		<project.build.jdk>1.7</project.build.jdk>
+		<maven.compiler.source>8</maven.compiler.source>
+		<maven.compiler.target>8</maven.compiler.target>
 	</properties>
 
 	<scm>
-        <connection>scm:git:https://gitee.com/qdbp/qdbp-able.git</connection>
-        <developerConnection>scm:git:https://gitee.com/qdbp/qdbp-able.git</developerConnection>
-        <url>https://gitee.com/qdbp/qdbp-able.git</url>
-        <tag>HEAD</tag>
-    </scm>
+		<connection>scm:git:https://gitee.com/qdbp/qdbp-able.git</connection>
+		<developerConnection>scm:git:https://gitee.com/qdbp/qdbp-able.git</developerConnection>
+		<url>https://gitee.com/qdbp/qdbp-able.git</url>
+		<tag>HEAD</tag>
+	</scm>
 
 	<distributionManagement>
 		<repository>
@@ -65,8 +68,8 @@
 				<artifactId>maven-compiler-plugin</artifactId>
 				<version>3.7.0</version>
 				<configuration>
-					<source>${project.build.jdk}</source>
-					<target>${project.build.jdk}</target>
+					<source>${maven.compiler.source}</source>
+					<target>${maven.compiler.target}</target>
 					<encoding>${project.build.sourceEncoding}</encoding>
 				</configuration>
 			</plugin>
@@ -113,14 +116,28 @@
 	</build>
 
 	<profiles>
+		<profile>
+			<id>jdk7</id>
+			<properties>
+				<maven.compiler.source>7</maven.compiler.source>
+				<maven.compiler.target>7</maven.compiler.target>
+			</properties>
+		</profile>
 		<profile>
 			<id>release</id>
+			<properties>
+				<maven.compiler.source>7</maven.compiler.source>
+				<maven.compiler.target>7</maven.compiler.target>
+			</properties>
 			<build>
 				<plugins>
 					<plugin>
 						<groupId>org.apache.maven.plugins</groupId>
 						<artifactId>maven-javadoc-plugin</artifactId>
 						<version>3.0.0</version>
+						<configuration>
+							<failOnError>false</failOnError>
+						</configuration>
 						<executions>
 							<execution>
 								<id>attach-javadocs</id>
diff --git a/test/pom.xml b/test/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5941e7e7c65299d44b167f6dc17b39f88d3c55e2
--- /dev/null
+++ b/test/pom.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-parent</artifactId>
+		<version>5.0.1</version>
+	</parent>
+
+	<artifactId>qdbp-json-test</artifactId>
+	<packaging>pom</packaging>
+	<version>5.5.15</version>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<modules>
+		<module>qdbp-json-test-base</module>
+		<module>qdbp-json-test-fastjson</module>
+		<module>qdbp-json-test-gson</module>
+		<module>qdbp-json-test-jackson</module>
+		<module>qdbp-tools-test-jdk7</module>
+	</modules>
+</project>
diff --git a/test/qdbp-json-test-base/pom.xml b/test/qdbp-json-test-base/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7ca5221a5bd854ebf89301d8f3b3a8a84fb2abdb
--- /dev/null
+++ b/test/qdbp-json-test-base/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.15</version>
+	</parent>
+
+	<artifactId>qdbp-json-test-base</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>jcl-over-slf4j</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>log4j-over-slf4j</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-core</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-classic</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.testng</groupId>
+			<artifactId>testng</artifactId>
+			<version>6.14.3</version>
+		</dependency>
+
+	</dependencies>
+</project>
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java
new file mode 100644
index 0000000000000000000000000000000000000000..ee0d28ffeb8d3e2c1abb143630c00e006f0a8201
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/TestCase1.java
@@ -0,0 +1,112 @@
+package com.gitee.qdbp.json.test;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.testng.Assert;
+import com.gitee.qdbp.json.JsonFeature;
+import com.gitee.qdbp.json.JsonService;
+import com.gitee.qdbp.json.test.entity.Address;
+import com.gitee.qdbp.json.test.entity.Child;
+import com.gitee.qdbp.json.test.entity.Father;
+import com.gitee.qdbp.json.test.entity.Gender;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
+
+/**
+ * TestCase1
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class TestCase1 {
+
+    public void test1() {
+        JsonService jsonService = JsonTools.findDefaultJsonService();
+        Assert.assertNotNull(jsonService);
+    }
+
+    public void test2() {
+        Father father = createTestData();
+
+        {
+            String jsonString = JsonTools.toJsonString(father, JsonFeature.serialization()
+                    .setQuoteFieldNames(false).setUseSingleQuotes(true).setHandleCircularReference(false));
+            System.out.println(jsonString);
+        }
+        {
+            String jsonString = JsonTools.toJsonString(father, JsonFeature.serialization()
+                    .setWriteNullNumberAsZero(true)
+                    .setWriteNullBooleanAsFalse(true)
+                    .setWriteNullStringAsEmpty(true)
+                    .setWriteNullListAsEmpty(true)
+                    .setWriteBigDecimalAsPlain(false)
+                    .setQuoteFieldNames(false)
+                    .setUseSingleQuotes(true)
+                    .setHandleCircularReference(false));
+            System.out.println(jsonString);
+        }
+    }
+
+    private Father createTestData() {
+        Address address = new Address();
+        address.setDetails("address1");
+
+        Father father = new Father();
+        father.setName("father1");
+        father.setGender(Gender.FEMALE);
+        father.setAge(40);
+        father.setHeight(170D);
+        father.setWeight(new BigDecimal("6.217e+18"));
+        father.addAddress(address);
+
+        Child child1 = new Child();
+        child1.setName("child1");
+        // child1.setFather(father);
+        child1.addAddress(address);
+        Child child2 = new Child();
+        child2.setName("child2");
+        // child2.setFather(father);
+        child2.addAddress(address);
+        father.addChild(child1);
+        father.addChild(child2);
+        return father;
+    }
+
+    public void test3() {
+        String jsonString = "{addresses:[{details:'address1'}],age:40,children:[{addresses:[{details:'address1'}],name:'child1'},{addresses:[{details:'address1'}],name:'child2'}],gender:'FEMALE',height:170,name:'father1',weight:75}";
+        Father father = JsonTools.parseAsObject(jsonString, Father.class);
+        Assert.assertNotNull(father);
+        Assert.assertEquals(father.getChildren().size(), 2);
+    }
+
+    public void test4() {
+        String jsonString = "[{addresses:[{details:'address1'}],name:'child1'},{addresses:[{details:'address1'}],name:'child2'}]";
+        List<Child> children = JsonTools.parseAsObjects(jsonString, Child.class);
+        Assert.assertNotNull(children);
+        Assert.assertEquals(children.size(), 2);
+        Assert.assertEquals(children.get(0).getClass(), Child.class);
+    }
+
+    @SuppressWarnings("deprecation")
+    public void test5() {
+        Father father = createTestData();
+        Map<String, Object> map = JsonTools.beanToMap(father);
+        Object children = map.get("children");
+        Assert.assertTrue(Collection.class.isAssignableFrom(children.getClass()));
+        List<Child> children1 = MapTools.getList(map, "children", Child.class);
+        Assert.assertEquals(children1.size(), 2);
+        Assert.assertEquals(JsonTools.getMapString(map, "name"), "father1");
+
+        Father newer = JsonTools.mapToBean(map, Father.class);
+        Assert.assertEquals(newer.getName(), "father1");
+        Assert.assertEquals(newer.getChildren().size(), 2);
+        Assert.assertEquals(newer.getChildren().get(0).getClass(), Child.class);
+    }
+
+    public void test6() {
+        int n1 = JsonTools.convert(null, int.class);
+        Assert.assertEquals(n1, 0);
+    }
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Address.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Address.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9cd96d966ff5e84faa1648254c0ccefe7bddd16
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Address.java
@@ -0,0 +1,20 @@
+package com.gitee.qdbp.json.test.entity;
+
+/**
+ * Address
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class Address {
+
+    private String details;
+
+    public String getDetails() {
+        return details;
+    }
+
+    public void setDetails(String details) {
+        this.details = details;
+    }
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Child.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Child.java
new file mode 100644
index 0000000000000000000000000000000000000000..587321b6b0958a68161a9aece21bfa146bedd6e0
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Child.java
@@ -0,0 +1,20 @@
+package com.gitee.qdbp.json.test.entity;
+
+/**
+ * Child
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class Child extends Person {
+
+    private Father father;
+
+    public Father getFather() {
+        return father;
+    }
+
+    public void setFather(Father father) {
+        this.father = father;
+    }
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Father.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Father.java
new file mode 100644
index 0000000000000000000000000000000000000000..8efa53c4f546abdabfea21376c7f609476e876d7
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Father.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.json.test.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Father
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class Father extends Person {
+
+    private List<Child> children = new ArrayList<>();
+
+    public List<Child> getChildren() {
+        return children;
+    }
+
+    public void setChildren(List<Child> children) {
+        this.children = children;
+    }
+
+    public void addChild(Child child) {
+        this.children.add(child);
+    }
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Gender.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Gender.java
new file mode 100644
index 0000000000000000000000000000000000000000..d778f8b6cc5c10cdcc80866f0a51b353785c5dfc
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Gender.java
@@ -0,0 +1,21 @@
+package com.gitee.qdbp.json.test.entity;
+
+/**
+ * Gender枚举类<br>
+ * 性别:0.未知|1.男|2.女
+ *
+ * @author zhh
+ * @version 150917
+ */
+public enum Gender {
+    
+    /** 0.未知 **/
+    UNKNOWN,
+    
+    /** 1.男 **/
+    MALE,
+    
+    /** 2.女 **/
+    FEMALE
+    
+}
diff --git a/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Person.java b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Person.java
new file mode 100644
index 0000000000000000000000000000000000000000..b0ab1df2b997f058eca6b92e39d94cbd3ff34d9d
--- /dev/null
+++ b/test/qdbp-json-test-base/src/main/java/com/gitee/qdbp/json/test/entity/Person.java
@@ -0,0 +1,78 @@
+package com.gitee.qdbp.json.test.entity;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Person
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+public class Person {
+
+    private String name;
+
+    private Gender gender;
+
+    private Integer age;
+
+    private Double height;
+
+    private BigDecimal weight;
+
+    private List<Address> addresses = new ArrayList<>();
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Gender getGender() {
+        return gender;
+    }
+
+    public void setGender(Gender gender) {
+        this.gender = gender;
+    }
+
+    public Integer getAge() {
+        return age;
+    }
+
+    public void setAge(Integer age) {
+        this.age = age;
+    }
+
+    public Double getHeight() {
+        return height;
+    }
+
+    public void setHeight(Double height) {
+        this.height = height;
+    }
+
+    public BigDecimal getWeight() {
+        return weight;
+    }
+
+    public void setWeight(BigDecimal weight) {
+        this.weight = weight;
+    }
+
+    public List<Address> getAddresses() {
+        return addresses;
+    }
+
+    public void setAddresses(List<Address> addresses) {
+        this.addresses = addresses;
+    }
+
+    public void addAddress(Address address) {
+        this.addresses.add(address);
+    }
+}
diff --git a/test/qdbp-json-test-fastjson/pom.xml b/test/qdbp-json-test-fastjson/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8aa4fccef29cfabde6d302200927ab0826e5aed6
--- /dev/null
+++ b/test/qdbp-json-test-fastjson/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.15</version>
+	</parent>
+
+	<artifactId>qdbp-json-test-fastjson</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json-test-base</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>1.2.16</version>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.12.4</version>
+				<configuration>
+					<suiteXmlFiles>
+						<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+					</suiteXmlFiles>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java b/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java
new file mode 100644
index 0000000000000000000000000000000000000000..73db2a1d2afd9ef3fb4be27b62003d6012bbad1f
--- /dev/null
+++ b/test/qdbp-json-test-fastjson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForFastJson.java
@@ -0,0 +1,48 @@
+package com.gitee.qdbp.json.test;
+
+import org.testng.annotations.Test;
+import com.alibaba.fastjson.JSON;
+
+/**
+ * TestCase1ForFastJson
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class TestCase1ForFastJson extends TestCase1 {
+    @Test
+    public void test0() {
+        System.out.println("fastjson version: " + JSON.VERSION);
+    }
+
+    @Test
+    public void test1() {
+        super.test1();
+    }
+
+    @Test
+    public void test2() {
+        super.test2();
+    }
+
+    @Test
+    public void test3() {
+        super.test3();
+    }
+
+    @Test
+    public void test4() {
+        super.test4();
+    }
+
+    @Test
+    public void test5() {
+        super.test5();
+    }
+
+    @Test
+    public void test6() {
+        super.test6();
+    }
+}
diff --git a/test/qdbp-json-test-fastjson/src/test/resources/logback.xml b/test/qdbp-json-test-fastjson/src/test/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4a622e2ebdd1093ba377f33035ee5e82d66cc579
--- /dev/null
+++ b/test/qdbp-json-test-fastjson/src/test/resources/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<configuration>
+	<property name="pattern.stdout" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\)%n" />
+	<property name="pattern.normal" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\) | %d{yyyy-MM-dd}%n" />
+
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${pattern.stdout}</pattern>
+		</encoder>
+	</appender>
+	<appender name="ManagerFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>../logs/qdbp-json-test-fastjson.log</file>
+		<!-- 按天滚动 保存30天-->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>../logs/qdbp-json-test-fastjson-%d{yyyyMMdd}-%i.log</FileNamePattern>
+			<MaxHistory>30</MaxHistory>
+			<!-- 超过指定大小滚动当天日志 -->
+			<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>50MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${pattern.normal}</pattern>
+		</encoder>
+	</appender>
+	<appender name ="ManagerAppender" class= "ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+		<queueSize>1024</queueSize>
+		<!-- 添加附加的appender,最多只能添加一个 -->
+	 	<appender-ref ref ="ManagerFile"/>
+	</appender>
+
+	<logger name="com.gitee.qdbp" level="DEBUG" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.sql.parse" level="ALL" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.plugins.impl.BaseSqlFileScanner" level="ALL" additivity="true" />
+	<!-- <logger name="org.springframework.orm.jpa" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="true" /> -->
+	<!-- <logger name="com.alibaba.druid.pool" level="DEBUG" additivity="true" /> -->
+
+	<!-- 只要使用了BeanPostProcessor的子类就很难避免此日志: is not eligible for getting processed by all BeanPostProcessors -->
+	<logger name="org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker" level="WARN" additivity="true" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ManagerAppender" />
+	</root>
+
+</configuration>
\ No newline at end of file
diff --git a/test/qdbp-json-test-fastjson/src/test/resources/testng.xml b/test/qdbp-json-test-fastjson/src/test/resources/testng.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9cba6061a83939e25cc93e9dd1a4b61d9a59f3ed
--- /dev/null
+++ b/test/qdbp-json-test-fastjson/src/test/resources/testng.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
+<suite name="JsonSuite" parallel="classes">
+	<test name="JsonTestForFastjson" verbose="2" preserve-order="true">
+		<packages>
+			<package name="com.gitee.qdbp.json.test" />
+		</packages>
+	</test>
+</suite>
diff --git a/test/qdbp-json-test-gson/pom.xml b/test/qdbp-json-test-gson/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..55df0db6fd04ffb80f23ca9ef5f427b2eee56229
--- /dev/null
+++ b/test/qdbp-json-test-gson/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.15</version>
+	</parent>
+
+	<artifactId>qdbp-json-test-gson</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json-test-base</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.google.code.gson</groupId>
+			<artifactId>gson</artifactId>
+			<version>2.10.1</version>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.12.4</version>
+				<configuration>
+					<suiteXmlFiles>
+						<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+					</suiteXmlFiles>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/test/qdbp-json-test-gson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForGson.java b/test/qdbp-json-test-gson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForGson.java
new file mode 100644
index 0000000000000000000000000000000000000000..c40bc9939b2015a2bf5f7d7a5a8d04e5c6e8cee9
--- /dev/null
+++ b/test/qdbp-json-test-gson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForGson.java
@@ -0,0 +1,43 @@
+package com.gitee.qdbp.json.test;
+
+import org.testng.annotations.Test;
+
+/**
+ * TestCase1ForGson
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class TestCase1ForGson extends TestCase1 {
+
+    @Test
+    public void test1() {
+        super.test1();
+    }
+
+    @Test
+    public void test2() {
+        super.test2();
+    }
+
+    @Test
+    public void test3() {
+        super.test3();
+    }
+
+    @Test
+    public void test4() {
+        super.test4();
+    }
+
+    @Test
+    public void test5() {
+        super.test5();
+    }
+
+    @Test
+    public void test6() {
+        super.test6();
+    }
+}
diff --git a/test/qdbp-json-test-gson/src/test/resources/logback.xml b/test/qdbp-json-test-gson/src/test/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..16509ee0bd3fd9fadf25c0d9645d670e64add235
--- /dev/null
+++ b/test/qdbp-json-test-gson/src/test/resources/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<configuration>
+	<property name="pattern.stdout" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\)%n" />
+	<property name="pattern.normal" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\) | %d{yyyy-MM-dd}%n" />
+
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${pattern.stdout}</pattern>
+		</encoder>
+	</appender>
+	<appender name="ManagerFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>../logs/qdbp-json-test-gson.log</file>
+		<!-- 按天滚动 保存30天-->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>../logs/qdbp-json-test-gson-%d{yyyyMMdd}-%i.log</FileNamePattern>
+			<MaxHistory>30</MaxHistory>
+			<!-- 超过指定大小滚动当天日志 -->
+			<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>50MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${pattern.normal}</pattern>
+		</encoder>
+	</appender>
+	<appender name ="ManagerAppender" class= "ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+		<queueSize>1024</queueSize>
+		<!-- 添加附加的appender,最多只能添加一个 -->
+	 	<appender-ref ref ="ManagerFile"/>
+	</appender>
+
+	<logger name="com.gitee.qdbp" level="DEBUG" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.sql.parse" level="ALL" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.plugins.impl.BaseSqlFileScanner" level="ALL" additivity="true" />
+	<!-- <logger name="org.springframework.orm.jpa" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="true" /> -->
+	<!-- <logger name="com.alibaba.druid.pool" level="DEBUG" additivity="true" /> -->
+
+	<!-- 只要使用了BeanPostProcessor的子类就很难避免此日志: is not eligible for getting processed by all BeanPostProcessors -->
+	<logger name="org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker" level="WARN" additivity="true" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ManagerAppender" />
+	</root>
+
+</configuration>
\ No newline at end of file
diff --git a/test/qdbp-json-test-gson/src/test/resources/testng.xml b/test/qdbp-json-test-gson/src/test/resources/testng.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d43b05682441d2d5e7597210960cb8de13f052e1
--- /dev/null
+++ b/test/qdbp-json-test-gson/src/test/resources/testng.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
+<suite name="JsonSuite" parallel="classes">
+	<test name="JsonTestForGson" verbose="2" preserve-order="true">
+		<packages>
+			<package name="com.gitee.qdbp.json.test" />
+		</packages>
+	</test>
+</suite>
diff --git a/test/qdbp-json-test-jackson/pom.xml b/test/qdbp-json-test-jackson/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3b7215fc7fa8a41d16265a9655eb5b1b91d51f87
--- /dev/null
+++ b/test/qdbp-json-test-jackson/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.15</version>
+	</parent>
+
+	<artifactId>qdbp-json-test-jackson</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp json test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json-test-base</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+			<version>2.7.9</version>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.12.4</version>
+				<configuration>
+					<suiteXmlFiles>
+						<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+					</suiteXmlFiles>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java b/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4803b6c93bd0d31bb938d2984f11c22ee1e46a5
--- /dev/null
+++ b/test/qdbp-json-test-jackson/src/test/java/com/gitee/qdbp/json/test/TestCase1ForJackson.java
@@ -0,0 +1,49 @@
+package com.gitee.qdbp.json.test;
+
+import org.testng.annotations.Test;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * TestCase1ForJackson
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class TestCase1ForJackson extends TestCase1 {
+    @Test
+    public void test0() {
+        Package pkg = ObjectMapper.class.getPackage();
+        System.out.println("jackson version: " + pkg.getImplementationVersion());
+    }
+
+    @Test
+    public void test1() {
+        super.test1();
+    }
+
+    @Test
+    public void test2() {
+        super.test2();
+    }
+
+    @Test
+    public void test3() {
+        super.test3();
+    }
+
+    @Test
+    public void test4() {
+        super.test4();
+    }
+
+    @Test
+    public void test5() {
+        super.test5();
+    }
+
+    @Test
+    public void test6() {
+        super.test6();
+    }
+}
diff --git a/test/qdbp-json-test-jackson/src/test/resources/logback.xml b/test/qdbp-json-test-jackson/src/test/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..acff406dc0dd89fac84f45d5772acd33b4f5a72c
--- /dev/null
+++ b/test/qdbp-json-test-jackson/src/test/resources/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<configuration>
+	<property name="pattern.stdout" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\)%n" />
+	<property name="pattern.normal" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\) | %d{yyyy-MM-dd}%n" />
+
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${pattern.stdout}</pattern>
+		</encoder>
+	</appender>
+	<appender name="ManagerFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>../logs/qdbp-json-test-jackson.log</file>
+		<!-- 按天滚动 保存30天-->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>../logs/qdbp-json-test-jackson-%d{yyyyMMdd}-%i.log</FileNamePattern>
+			<MaxHistory>30</MaxHistory>
+			<!-- 超过指定大小滚动当天日志 -->
+			<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>50MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${pattern.normal}</pattern>
+		</encoder>
+	</appender>
+	<appender name ="ManagerAppender" class= "ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+		<queueSize>1024</queueSize>
+		<!-- 添加附加的appender,最多只能添加一个 -->
+	 	<appender-ref ref ="ManagerFile"/>
+	</appender>
+
+	<logger name="com.gitee.qdbp" level="DEBUG" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.sql.parse" level="ALL" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.plugins.impl.BaseSqlFileScanner" level="ALL" additivity="true" />
+	<!-- <logger name="org.springframework.orm.jpa" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="true" /> -->
+	<!-- <logger name="com.alibaba.druid.pool" level="DEBUG" additivity="true" /> -->
+
+	<!-- 只要使用了BeanPostProcessor的子类就很难避免此日志: is not eligible for getting processed by all BeanPostProcessors -->
+	<logger name="org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker" level="WARN" additivity="true" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ManagerAppender" />
+	</root>
+
+</configuration>
\ No newline at end of file
diff --git a/test/qdbp-json-test-jackson/src/test/resources/testng.xml b/test/qdbp-json-test-jackson/src/test/resources/testng.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d37209d9e9a664ebfc9615b4ab0499718ceb48bb
--- /dev/null
+++ b/test/qdbp-json-test-jackson/src/test/resources/testng.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
+<suite name="JsonSuite" parallel="classes">
+	<test name="JsonTestForJackson" verbose="2" preserve-order="true">
+		<packages>
+			<package name="com.gitee.qdbp.json.test" />
+		</packages>
+	</test>
+</suite>
diff --git a/test/qdbp-tools-test-jdk7/pom.xml b/test/qdbp-tools-test-jdk7/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ecebe9314cad48c5d74591169442f64c9b33bc86
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/pom.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.gitee.qdbp</groupId>
+		<artifactId>qdbp-json-test</artifactId>
+		<version>5.5.15</version>
+	</parent>
+
+	<artifactId>qdbp-tools-test-jdk7</artifactId>
+	<packaging>jar</packaging>
+	<url>https://gitee.com/qdbp/qdbp-able/</url>
+	<description>qdbp tools test</description>
+
+	<properties>
+		<maven.compiler.source>7</maven.compiler.source>
+		<maven.compiler.target>7</maven.compiler.target>
+	</properties>
+
+	<dependencies>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-able</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-tools</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>1.2.16</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>jcl-over-slf4j</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>log4j-over-slf4j</artifactId>
+			<version>1.7.25</version>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-core</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-classic</artifactId>
+			<version>1.2.3</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.testng</groupId>
+			<artifactId>testng</artifactId>
+			<version>6.14.3</version>
+		</dependency>
+
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.12.4</version>
+				<configuration>
+					<suiteXmlFiles>
+						<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
+					</suiteXmlFiles>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9beb3da6143141a7d8637512134861797862c4d8
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/ConvertToolsTest.java
@@ -0,0 +1,99 @@
+package com.gitee.qdbp.tools.test;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+
+/**
+ * 数据转换工具类
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class ConvertToolsTest {
+
+    @Test
+    public void test1() {
+        long value = ConvertTools.toLong("1024 * 1024");
+        Assert.assertEquals(value, 1024 * 1024);
+    }
+
+    @Test
+    public void test2() {
+        long value = ConvertTools.toLong("10:20:30.456");
+        Assert.assertEquals(value, 10*60*60*1000+20*60*1000+30*1000+456);
+    }
+
+    @Test
+    public void test3() {
+        long value = ConvertTools.toLong("16KB");
+        Assert.assertEquals(value, 16*1024);
+    }
+
+    @Test
+    public void test4() {
+        long value = ConvertTools.toLong("1024");
+        Assert.assertEquals(value, 1024);
+    }
+
+    @Test
+    public void test5() {
+        long value = ConvertTools.toLong("-1024");
+        Assert.assertEquals(value, -1024);
+    }
+
+    @Test
+    public void test6() {
+        try {
+            // Duration since jdk8
+            Class.forName("java.time.Duration");
+
+            long value = ConvertTools.toLong("PT15H");
+            Assert.assertEquals(value, 15*60*60*1000);
+        } catch (ClassNotFoundException ignore) {
+            System.out.println("test6 skipped on jdk7 ......");
+        }
+    }
+
+    @Test
+    public void test7() {
+        Map<Integer, Object> map = new LinkedHashMap<>();
+        map.put(0, "A");
+        map.put(1, "B");
+        map.put(2, "C");
+        List<Object> objects = ConvertTools.parseList(map);
+        Assert.assertEquals(objects.size(), 3);
+        Assert.assertEquals(objects.get(0), "A");
+        System.out.println(objects);
+    }
+
+    @Test
+    public void test8() {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("0", "A");
+        map.put("1", "B");
+        map.put("2", "C");
+        List<Object> objects = ConvertTools.parseList(map);
+        Assert.assertEquals(objects.size(), 3);
+        Assert.assertEquals(objects.get(0), "A");
+        System.out.println(objects);
+    }
+
+    @Test
+    public void test9() {
+        Map<Integer, Object> map = new HashMap<>();
+        map.put(0, "A");
+        map.put(1, "B");
+        map.put(2, "C");
+        List<Object> objects = ConvertTools.parseList(map);
+        Assert.assertEquals(objects.size(), 1);
+        Assert.assertSame(objects.get(0), map);
+        Assert.assertEquals(objects.get(0).getClass(), HashMap.class);
+        System.out.println(objects);
+    }
+}
diff --git a/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/EachMapTest.java b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/EachMapTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..92506e2fd85e938d2409ec794762f8ea80041ec3
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/src/test/java/com/gitee/qdbp/tools/test/EachMapTest.java
@@ -0,0 +1,74 @@
+package com.gitee.qdbp.tools.test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.beans.MapPlus;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.ReflectTools;
+
+/**
+ * 深度遍历Map测试
+ *
+ * @author zhaohuihua
+ * @version 20230215
+ */
+@Test
+public class EachMapTest {
+
+    @Test
+    public void test() {
+        List<Map<String, Object>> addresses = new ArrayList<>();
+        MapPlus address = new MapPlus();
+        address.put("city", "nanjing");
+        address.put("phones", Arrays.asList("1301112222", "13044445555"));
+        addresses.add(address);
+
+        MapPlus root = new MapPlus();
+        root.put("entity.user.userName", "test1");
+        root.put("entity.user.address", addresses);
+        System.out.println(JsonTools.toLogString(root));
+        Assert.assertEquals(root.size(), 1);
+        Map<String, Object> entity = root.getSubMap("entity");
+        System.out.println(entity.getClass());
+        Assert.assertEquals(entity.getClass(), MapPlus.class);
+
+        List<Map<String, Object>> list = root.getSubMaps("entity.user.address");
+        System.out.println(list.getClass());
+        Assert.assertEquals(list.size(), 1);
+        Assert.assertSame(list, addresses);
+        Map<String, Object> address1 = list.get(0);
+        System.out.println(address1.getClass());
+        Assert.assertEquals(address1.getClass(), MapPlus.class);
+        Assert.assertSame(address1, address);
+
+        final Map<String, Object> result = new LinkedHashMap<>();
+        MapTools.each(root, new MapTools.EachInterceptor() {
+            @Override
+            public void intercept(MapTools.FieldEntry entry) {
+                String fieldPath = entry.getFieldPath();
+                Object fieldValue = entry.getFieldValue();
+                if (fieldValue == null) {
+                    result.put(fieldPath, null);
+                } else if (ReflectTools.isPrimitive(fieldValue.getClass(), false)) {
+                    result.put(fieldPath, fieldValue);
+                }
+            }
+        });
+        System.out.println(JsonTools.toLogString(result));
+        Assert.assertTrue(result.containsKey("entity.user.userName"));
+        Assert.assertTrue(result.containsKey("entity.user.address[0].city"));
+        Assert.assertTrue(result.containsKey("entity.user.address[0].phones[0]"));
+        Assert.assertTrue(result.containsKey("entity.user.address[0].phones[1]"));
+
+        Assert.assertEquals(result.get("entity.user.userName"), "test1");
+        Assert.assertEquals(result.get("entity.user.address[0].city"), "nanjing");
+        Assert.assertEquals(result.get("entity.user.address[0].phones[0]"), "1301112222");
+        Assert.assertEquals(result.get("entity.user.address[0].phones[1]"), "13044445555");
+    }
+}
diff --git a/test/qdbp-tools-test-jdk7/src/test/resources/logback.xml b/test/qdbp-tools-test-jdk7/src/test/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..acff406dc0dd89fac84f45d5772acd33b4f5a72c
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/src/test/resources/logback.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<configuration>
+	<property name="pattern.stdout" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\)%n" />
+	<property name="pattern.normal" value=">>%5p %d{HH:mm:ss.SSS} | %m | %t | %C.%M\\(%F:%L\\) | %d{yyyy-MM-dd}%n" />
+
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder charset="UTF-8">
+			<pattern>${pattern.stdout}</pattern>
+		</encoder>
+	</appender>
+	<appender name="ManagerFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>../logs/qdbp-json-test-jackson.log</file>
+		<!-- 按天滚动 保存30天-->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>../logs/qdbp-json-test-jackson-%d{yyyyMMdd}-%i.log</FileNamePattern>
+			<MaxHistory>30</MaxHistory>
+			<!-- 超过指定大小滚动当天日志 -->
+			<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<MaxFileSize>50MB</MaxFileSize>
+			</TimeBasedFileNamingAndTriggeringPolicy>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${pattern.normal}</pattern>
+		</encoder>
+	</appender>
+	<appender name ="ManagerAppender" class= "ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
+		<queueSize>1024</queueSize>
+		<!-- 添加附加的appender,最多只能添加一个 -->
+	 	<appender-ref ref ="ManagerFile"/>
+	</appender>
+
+	<logger name="com.gitee.qdbp" level="DEBUG" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.sql.parse" level="ALL" additivity="true" />
+	<logger name="com.gitee.qdbp.jdbc.plugins.impl.BaseSqlFileScanner" level="ALL" additivity="true" />
+	<!-- <logger name="org.springframework.orm.jpa" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction" level="DEBUG" additivity="true" /> -->
+	<!-- <logger name="org.springframework.transaction.interceptor" level="TRACE" additivity="true" /> -->
+	<!-- <logger name="com.alibaba.druid.pool" level="DEBUG" additivity="true" /> -->
+
+	<!-- 只要使用了BeanPostProcessor的子类就很难避免此日志: is not eligible for getting processed by all BeanPostProcessors -->
+	<logger name="org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker" level="WARN" additivity="true" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ManagerAppender" />
+	</root>
+
+</configuration>
\ No newline at end of file
diff --git a/test/qdbp-tools-test-jdk7/src/test/resources/testng.xml b/test/qdbp-tools-test-jdk7/src/test/resources/testng.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3f55d761b238cd0b45b59ca67b8b03aedd284a94
--- /dev/null
+++ b/test/qdbp-tools-test-jdk7/src/test/resources/testng.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
+<suite name="QdbpToolsSuite" parallel="classes">
+	<test name="QdbpToolsTest" verbose="2" preserve-order="true">
+		<packages>
+			<package name="com.gitee.qdbp.tools.test" />
+		</packages>
+	</test>
+</suite>
diff --git a/tools/pom.xml b/tools/pom.xml
index f453d73a46d2524af09761610bef56881d54cc71..057e332238b95e774cb50676f620101f304d22cf 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -5,12 +5,12 @@
 	<parent>
 		<groupId>com.gitee.qdbp</groupId>
 		<artifactId>qdbp-parent</artifactId>
-		<version>5.0.1</version>
+		<version>5.0.2</version>
 	</parent>
 
 	<artifactId>qdbp-tools</artifactId>
 	<packaging>jar</packaging>
-	<version>5.4.6</version>
+	<version>5.5.15</version>
 	<url>https://gitee.com/qdbp/qdbp-able/</url>
 	<description>qdbp tools library</description>
 
@@ -25,13 +25,33 @@
 			<groupId>com.gitee.qdbp</groupId>
 			<artifactId>qdbp-able</artifactId>
 			<version>${project.version}</version>
-			<optional>true</optional>
+		</dependency>
+
+		<dependency>
+			<groupId>com.gitee.qdbp</groupId>
+			<artifactId>qdbp-json</artifactId>
+			<version>${project.version}</version>
 		</dependency>
 
 		<dependency>
 			<groupId>com.alibaba</groupId>
 			<artifactId>fastjson</artifactId>
-			<version>1.2.76</version>
+			<version>1.2.83</version>
+			<optional>true</optional>
+		</dependency>
+
+		<dependency>
+			<groupId>org.yaml</groupId>
+			<artifactId>snakeyaml</artifactId>
+			<version>1.33</version>
+			<optional>true</optional>
+		</dependency>
+
+		<!-- SFTP -->
+		<dependency>
+			<groupId>com.jcraft</groupId>
+			<artifactId>jsch</artifactId>
+			<version>0.1.55</version>
 			<optional>true</optional>
 		</dependency>
 
@@ -140,7 +160,7 @@
 			<version>1.2.3</version>
 			<scope>test</scope>
 		</dependency>
-		
+
 		<dependency>
 			<groupId>org.testng</groupId>
 			<artifactId>testng</artifactId>
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/cache/BaseCacheService.java b/tools/src/main/java/com/gitee/qdbp/tools/cache/BaseCacheService.java
index b12cee5a227ed333fc8a77e7b08601d6abd8cdaa..e888d4e865130fb173cae422dc0273fd2b75c3f4 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/cache/BaseCacheService.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/cache/BaseCacheService.java
@@ -4,13 +4,12 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.beans.Duration;
 import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 基础缓存类
@@ -58,22 +57,22 @@ public abstract class BaseCacheService extends AbstractCacheService {
     }
 
     protected <T> Map<String, String> serializeFields(T value) {
-        Object object = JSON.toJSON(value);
-        if (object instanceof JSONObject) {
-            JSONObject json = (JSONObject) object;
-            Map<String, String> map = new HashMap<>();
-            for (Map.Entry<String, Object> entry : json.entrySet()) {
-                if (VerifyTools.isNotBlank(entry.getValue())) {
-                    map.put(entry.getKey(), serializeValue(entry.getValue()));
-                }
+        Map<String, Object> json;
+        try {
+            json = JsonTools.beanToMap(value);
+        } catch (Exception e){
+            throw new IllegalArgumentException("value must be a plain object", e);
+        }
+        Map<String, String> map = new HashMap<>();
+        for (Map.Entry<String, Object> entry : json.entrySet()) {
+            if (VerifyTools.isNotBlank(entry.getValue())) {
+                map.put(entry.getKey(), serializeValue(entry.getValue()));
             }
-            return map;
-        } else {
-            throw new IllegalArgumentException("value must be a plain object");
         }
+        return map;
     }
 
-    protected <T> T deserializeFeilds(Map<String, String> map, Class<T> clazz) {
+    protected <T> T deserializeFields(Map<String, String> map, Class<T> clazz) {
         StringBuilder buffer = new StringBuilder();
         for (Entry<String, String> entry : map.entrySet()) {
             if (VerifyTools.isAnyBlank(entry.getKey(), entry.getValue())) continue;
@@ -85,13 +84,13 @@ public abstract class BaseCacheService extends AbstractCacheService {
             } else if (value.startsWith("[") && value.endsWith("]")) { // 数组
                 buffer.append(value);
             } else { // 普通字符串
-                buffer.append(JSON.toJSONString(value)); // 替换字符串中的引号反斜杠
+                buffer.append(JsonTools.toJsonString(value)); // 替换字符串中的引号反斜杠
             }
         }
         String string = "{" + buffer + "}";
 
         try {
-            return JSON.parseObject(string, clazz);
+            return JsonTools.parseAsObject(string, clazz);
         } catch (Exception e) {
             log.error("JsonParseError:{}, class={}, text={}", e, clazz.getSimpleName(), string);
             throw e;
@@ -106,7 +105,7 @@ public abstract class BaseCacheService extends AbstractCacheService {
         } else if (value instanceof Enum) {
             return ((Enum<?>) value).name();
         } else {
-            return JSON.toJSONString(value);
+            return JsonTools.toJsonString(value);
         }
     }
 
@@ -118,7 +117,7 @@ public abstract class BaseCacheService extends AbstractCacheService {
             return (T) string;
         } else {
             try {
-                return JSON.parseObject(string, clazz);
+                return JsonTools.parseAsObject(string, clazz);
             } catch (Exception e) {
                 log.error("JsonParseError:{}, class={}, text={}", e, clazz.getSimpleName(), string);
                 throw e;
@@ -131,7 +130,7 @@ public abstract class BaseCacheService extends AbstractCacheService {
             return null;
         } else {
             try {
-                return JSON.parseArray(string, clazz);
+                return JsonTools.parseAsObjects(string, clazz);
             } catch (Exception e) {
                 log.error("JsonParseError:{}, class={}, text={}", e, clazz.getSimpleName(), string);
                 throw e;
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/cache/InMemoryCache.java b/tools/src/main/java/com/gitee/qdbp/tools/cache/InMemoryCache.java
index 993bf3bcda1592aa05bd6772fb109637caacacc3..a8e1279dede9b426093cb83ed1149c2b3c779a83 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/cache/InMemoryCache.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/cache/InMemoryCache.java
@@ -7,9 +7,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.beans.VolatileData;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -252,7 +251,7 @@ public class InMemoryCache extends BaseCacheService {
         if (hash.isEmpty()) {
             return null;
         } else {
-            return deserializeFeilds(hash, clazz);
+            return deserializeFields(hash, clazz);
         }
     }
 
@@ -305,7 +304,7 @@ public class InMemoryCache extends BaseCacheService {
     @SuppressWarnings("unchecked")
     protected <T> List<T> castToList(String key, Object value, Class<T> clazz) {
         if (value instanceof String) {
-            return JSONArray.parseArray((String) value, clazz);
+            return JsonTools.parseAsObjects((String) value, clazz);
         } else if (value instanceof Collection) {
             if (value instanceof List) { // value就是List对象
                 List<?> list = (List<?>) value;
@@ -325,7 +324,7 @@ public class InMemoryCache extends BaseCacheService {
             List<T> list = new ArrayList<>();
             Collection<?> values = (Collection<?>) value;
             for (Object i : values) { // 逐一转换为目标类
-                list.add(TypeUtils.castToJavaBean(i, clazz));
+                list.add(JsonTools.convert(i, clazz));
             }
             return list;
         } else {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java
new file mode 100644
index 0000000000000000000000000000000000000000..49ceba74c7713482021e12d10a7e6948c5ae7ad7
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsBean.java
@@ -0,0 +1,50 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.tools.excel.model.RowInfo;
+import com.gitee.qdbp.tools.utils.JsonTools;
+
+/**
+ * Excel内容导入为JavaBean
+ *
+ * @author zhaohuihua
+ * @version 20220612
+ */
+public class ImportAsBean<T> extends ImportCallback {
+
+    /** 版本序列号 **/
+    private static final long serialVersionUID = 1L;
+
+    private final Class<T> beanClass;
+    private final List<T> contents = new ArrayList<>();
+
+    /**
+     * 构造函数
+     *
+     * @param beanClass Bean的类型
+     */
+    public ImportAsBean(Class<T> beanClass) {
+        this.beanClass = beanClass;
+    }
+
+    /**
+     * 从Excel导入的内容
+     * @return JavaBean列表
+     */
+    public List<T> getContents() {
+        return contents;
+    }
+
+    @Override
+    public void callback(Map<String, Object> map, RowInfo row) throws ServiceException {
+        try {
+            T bean = JsonTools.mapToBean(map, beanClass);
+            contents.add(bean);
+        } catch (Exception e) {
+            throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
+        }
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsMap.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..f044699b97c4c364c34f58c3460fa377fc6c39e5
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportAsMap.java
@@ -0,0 +1,33 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.excel.model.RowInfo;
+
+/**
+ * Excel内容导入为JavaBean
+ *
+ * @author zhaohuihua
+ * @version 20220612
+ */
+public class ImportAsMap extends ImportCallback {
+
+    /** 版本序列号 **/
+    private static final long serialVersionUID = 1L;
+
+    private final List<Map<String, Object>> contents = new ArrayList<>();
+
+    /**
+     * 从Excel导入的内容
+     * @return JavaBean列表
+     */
+    public List<Map<String, Object>> getContents() {
+        return contents;
+    }
+
+    @Override
+    public void callback(Map<String, Object> map, RowInfo row) {
+        contents.add(map);
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportResult.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..5df1d8263154b80ebb99fe3feb8947a2e00c5e87
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/ImportResult.java
@@ -0,0 +1,46 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.io.Serializable;
+import java.util.List;
+import com.gitee.qdbp.able.result.IBatchResult;
+import com.gitee.qdbp.tools.excel.model.FailedInfo;
+
+/**
+ * 导入结果
+ *
+ * @author zhaohuihua
+ * @version 160302
+ */
+public class ImportResult<T> implements IBatchResult<FailedInfo>, Serializable {
+
+    /** 版本序列号 **/
+    private static final long serialVersionUID = 1L;
+
+    /** 导入成功的内容(成功列表) **/
+    private final List<T> contents;
+    /** 导入实际结果 **/
+    private final SheetParseResult callback;
+
+    protected ImportResult(List<T> contents, SheetParseResult callback) {
+        this.contents = contents;
+        this.callback = callback;
+    }
+
+    /** 获取记录总数 **/
+    @Override
+    public Integer getTotal() {
+        return callback.getTotal();
+    }
+
+    /** 导入成功的内容(成功列表) **/
+    public List<T> getContents() {
+        return contents;
+    }
+
+    /** 失败列表 **/
+    @Override
+    public List<FailedInfo> getFailed() {
+        return callback.getFailed();
+    }
+
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
index e3ec7988ce89615bff68eb3362a2415270cf7bd4..92966a51d43c8de2a084f3d52a2a6e7c52c1ae0b 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetFillCallback.java
@@ -4,20 +4,20 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.excel.rule.CellRule;
 import com.gitee.qdbp.tools.excel.rule.IgnoreIllegalValue;
 import com.gitee.qdbp.tools.excel.utils.ExcelTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Cell;
-import org.apache.poi.ss.usermodel.Row;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Sheet填充回调函数
@@ -49,7 +49,7 @@ public class SheetFillCallback implements Serializable {
 
     /** JavaBean转换为Map **/
     public Map<String, Object> toMap(Object value) {
-        return (JSONObject) JSON.toJSON(value);
+        return JsonTools.beanToMap(value);
     }
 
     /** 单元格字段转换, data.put(cellInfo.getField(), cellInfo.getValue()); **/
@@ -80,9 +80,12 @@ public class SheetFillCallback implements Serializable {
                     }
                 } catch (Exception e) {
                     if (!ignoreIllegalValue) {
-                        String msg = ExcelTools.newConvertErrorMessage(cellInfo, rule);
-                        Exception se = new IllegalArgumentException(msg, e);
-                        throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, se);
+                        String details = ExcelTools.newConvertErrorMessage(cellInfo, rule);
+                        if (e instanceof IResultException) {
+                            throw new ServiceException((IResultException) e).setDetails(details);
+                        } else {
+                            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR).setDetails(details);
+                        }
                     } else {
                         cellInfo.setValue(null);
                         if (log.isWarnEnabled()) {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
index 4015eedf07dc8e3721795517e40750e988776718..4e5ee8086293d864cd38f793545ff7c2a7df2ef1 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseCallback.java
@@ -4,21 +4,20 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import org.apache.poi.ss.usermodel.Cell;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
-import com.gitee.qdbp.able.result.IBatchResult;
+import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.IResultMessage;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
-import com.gitee.qdbp.tools.excel.model.FailedInfo;
 import com.gitee.qdbp.tools.excel.model.FieldInfo;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.excel.rule.CellRule;
 import com.gitee.qdbp.tools.excel.rule.IgnoreIllegalValue;
 import com.gitee.qdbp.tools.excel.utils.ExcelTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Cell;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 解析回调函数
@@ -26,7 +25,7 @@ import org.slf4j.LoggerFactory;
  * @author zhaohuihua
  * @version 160302
  */
-public abstract class SheetParseCallback implements IBatchResult, Serializable {
+public abstract class SheetParseCallback extends SheetParseResult implements Serializable {
 
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
@@ -34,41 +33,28 @@ public abstract class SheetParseCallback implements IBatchResult, Serializable {
     /** 日志对象 **/
     private final Logger log = LoggerFactory.getLogger(this.getClass());
 
-    /** 记录总数 **/
-    private Integer total = 0;
-    /** 失败列表 **/
-    private final List<Failed> failed = new ArrayList<>();
-
     /** 整行失败 **/
+    @Override
     public void addFailed(String sheetName, int row, IResultMessage result) {
-        failed.add(new FailedInfo(sheetName, row, result));
+        super.addFailed(sheetName, row, result);
     }
 
     /** 具体某一列失败(注意:字段名和字段值都填在field这里, value的值不用于前端显示) **/
-    public void addFailed(String sheetName, int row, String field, Object value, IResultMessage result) {
-        failed.add(new FailedInfo(sheetName, row, field, value, result));
-    }
-
-    /** 失败列表 **/
     @Override
-    public List<Failed> getFailed() {
-        return failed;
+    public void addFailed(String sheetName, int row, String field, Object value, IResultMessage result) {
+        super.addFailed(sheetName, row, field, value, result);
     }
 
     /** 增加记录总数 **/
+    @Override
     public void addTotal(int number) {
-        total += number;
+        super.addTotal(number);
     }
 
     /** 设置记录总数 **/
-    public void setTotal(int total) {
-        this.total = total;
-    }
-
-    /** 获取记录总数 **/
     @Override
-    public Integer getTotal() {
-        return total;
+    public void setTotal(int total) {
+        super.setTotal(total);
     }
 
     /** 具体的业务处理逻辑, 一般是插入数据库之类的持久化操作 **/
@@ -107,15 +93,18 @@ public abstract class SheetParseCallback implements IBatchResult, Serializable {
                     }
                 } catch (Exception e) {
                     if (!ignoreIllegalValue) {
-                        String msg = ExcelTools.newConvertErrorMessage(cellInfo, rule);
-                        Exception se = new IllegalArgumentException(msg, e);
-                        throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR, se);
+                        String details = ExcelTools.newConvertErrorMessage(cellInfo, rule);
+                        if (e instanceof IResultException) {
+                            throw new ServiceException((IResultException) e).setDetails(details);
+                        } else {
+                            throw new ServiceException(ResultCode.PARAMETER_VALUE_ERROR).setDetails(details);
+                        }
                     } else {
+                        cellInfo.setValue(null);
                         if (log.isWarnEnabled()) {
                             String msg = ExcelTools.newConvertErrorMessage(cellInfo, rule);
-                            log.warn(msg + ", " + e);
+                            log.warn(msg + ", " + e.getMessage());
                         }
-                        cellInfo.setValue(null);
                     }
                 }
             }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseResult.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e380d4b98023863fbd8041d2ab68ab2cfd430cf
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/SheetParseResult.java
@@ -0,0 +1,58 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import com.gitee.qdbp.able.result.IBatchResult;
+import com.gitee.qdbp.able.result.IResultMessage;
+import com.gitee.qdbp.tools.excel.model.FailedInfo;
+
+/**
+ * 解析结果
+ *
+ * @author zhaohuihua
+ * @version 160302
+ */
+public class SheetParseResult implements IBatchResult<FailedInfo>, Serializable {
+
+    /** 版本序列号 **/
+    private static final long serialVersionUID = 1L;
+
+    /** 记录总数 **/
+    private Integer total = 0;
+    /** 失败列表 **/
+    private final List<FailedInfo> failed = new ArrayList<>();
+
+    /** 整行失败 **/
+    protected void addFailed(String sheetName, int row, IResultMessage result) {
+        failed.add(new FailedInfo(sheetName, row, result));
+    }
+
+    /** 具体某一列失败(注意:字段名和字段值都填在field这里, value的值不用于前端显示) **/
+    protected void addFailed(String sheetName, int row, String field, Object value, IResultMessage result) {
+        failed.add(new FailedInfo(sheetName, row, field, value, result));
+    }
+
+    /** 失败列表 **/
+    @Override
+    public List<FailedInfo> getFailed() {
+        return failed;
+    }
+
+    /** 增加记录总数 **/
+    protected void addTotal(int number) {
+        total += number;
+    }
+
+    /** 设置记录总数 **/
+    protected void setTotal(int total) {
+        this.total = total;
+    }
+
+    /** 获取记录总数 **/
+    @Override
+    public Integer getTotal() {
+        return total;
+    }
+
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
index 5a3f7ea1b5ebce44cc04642ffafe83c46298f21d..bb33bdc06966c789f0d5bf5d9b60a77d5faa0f18 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelExporter.java
@@ -1,12 +1,14 @@
 package com.gitee.qdbp.tools.excel;
 
+import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.URL;
 import java.util.List;
-import org.apache.poi.POIXMLException;
-import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
@@ -15,11 +17,12 @@ import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.enums.FileErrorCode;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.utils.ExcelHelper;
-import com.gitee.qdbp.tools.utils.VerifyTools;
+import com.gitee.qdbp.tools.files.FileTools;
+import com.gitee.qdbp.tools.files.PathTools;
 
 /**
  * Excel导出
- * 
+ *
  * @author zhaohuihua
  * @version 160302
  */
@@ -29,84 +32,254 @@ public class XExcelExporter {
 
     private final XMetadata metadata;
 
-    private ExportCallback callback;
-
     public XExcelExporter(XMetadata metadata) {
         this.metadata = metadata;
-        this.callback = new ExportCallback();
     }
 
     /**
      * Excel导出
-     * 
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件路径
+     * @param saveFile 保存的文件路径
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, File templateFile, File saveFile) throws ServiceException {
+        FileTools.mkdirsIfNotExists(saveFile);
+        try (InputStream input = new FileInputStream(templateFile)) {
+            try (OutputStream output = new FileOutputStream(saveFile)) {
+                export(data, input, output, null);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(saveFile.getPath());
+            } catch (IOException e) {
+                log.error("write excel error.", e);
+                throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR, e);
+            }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(templateFile.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param saveFile 保存的文件路径
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, URL templateFile, File saveFile) throws ServiceException {
+        FileTools.mkdirsIfNotExists(saveFile);
+        try (InputStream input = templateFile.openStream()) {
+            try (OutputStream output = new FileOutputStream(saveFile)) {
+                export(data, input, output, null);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(saveFile.getPath());
+            } catch (IOException e) {
+                log.error("write excel error.", e);
+                throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR, e);
+            }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(templateFile));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
      * @param data 待导出的数据
-     * @param templatePath 模板文件的路径
+     * @param templateFile 模板文件
      * @param output 输出流
      * @throws ServiceException 处理失败
      */
-    public void export(List<?> data, String templatePath, OutputStream output) throws ServiceException {
-        this.export(data, templatePath, output, null);
+    public void export(List<?> data, File templateFile, OutputStream output) throws ServiceException {
+        try (InputStream input = new FileInputStream(templateFile)) {
+            this.export(data, input, output, null);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(templateFile.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
     }
 
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, URL templateFile, OutputStream output) throws ServiceException {
+        try (InputStream input = templateFile.openStream()) {
+            this.export(data, input, output, null);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(templateFile));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param template 模板文件输入流
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
     public void export(List<?> data, InputStream template, OutputStream output) throws ServiceException {
         this.export(data, template, output, null);
     }
 
-    public void export(List<?> data, String templatePath, OutputStream output, ExportCallback cb)
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件路径
+     * @param saveFile 保存的文件路径
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, File templateFile, File saveFile, ExportCallback callback)
+            throws ServiceException {
+        FileTools.mkdirsIfNotExists(saveFile);
+        try (InputStream input = new FileInputStream(templateFile)) {
+            try (OutputStream output = new FileOutputStream(saveFile)) {
+                export(data, input, output, callback);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(saveFile.getPath());
+            } catch (IOException e) {
+                log.error("write excel error.", e);
+                throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR, e);
+            }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(templateFile.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param saveFile 保存的文件路径
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, URL templateFile, File saveFile, ExportCallback callback) throws ServiceException {
+        FileTools.mkdirsIfNotExists(saveFile);
+        try (InputStream input = templateFile.openStream()) {
+            try (OutputStream output = new FileOutputStream(saveFile)) {
+                export(data, input, output, callback);
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(saveFile.getPath());
+            } catch (IOException e) {
+                log.error("write excel error.", e);
+                throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR, e);
+            }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(templateFile));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, File templateFile, OutputStream output, ExportCallback callback)
+            throws ServiceException {
+        try (InputStream input = new FileInputStream(templateFile)) {
+            this.export(data, input, output, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(templateFile.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * Excel导出
+     *
+     * @param data 待导出的数据
+     * @param templateFile 模板文件
+     * @param output 输出流
+     * @throws ServiceException 处理失败
+     */
+    public void export(List<?> data, URL templateFile, OutputStream output, ExportCallback callback)
             throws ServiceException {
-        try (InputStream input = new FileInputStream(templatePath)) {
-            export(data, input, output, cb);
+        try (InputStream input = templateFile.openStream()) {
+            this.export(data, input, output, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(templateFile));
         } catch (IOException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
         }
     }
 
-    public void export(List<?> data, InputStream template, OutputStream output, ExportCallback cb)
+    public void export(List<?> data, InputStream template, OutputStream output, ExportCallback callback)
             throws ServiceException {
 
-        if (cb == null) cb = this.callback;
+        if (callback == null) {
+            callback = new ExportCallback();
+        }
 
         try (Workbook wb = WorkbookFactory.create(template)) {
 
-            cb.init(wb, metadata);
+            callback.init(wb, metadata);
             Sheet sheet = wb.getSheetAt(0);
 
-            if (cb.onSheetStart(sheet, metadata, data)) {
-                ExcelHelper.fill(data, sheet, metadata, cb);
-                cb.onSheetFinished(sheet, metadata, data);
+            if (callback.onSheetStart(sheet, metadata, data)) {
+                ExcelHelper.fill(data, sheet, metadata, callback);
+                callback.onSheetFinished(sheet, metadata, data);
             }
 
             // 计算所有公式
             // sheet.setForceFormulaRecalculation(true); // 设置Excel打开的时候计算
             wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); // 事先计算
 
-            cb.finish(wb, metadata);
+            callback.finish(wb, metadata);
 
             try {
                 wb.write(output);
-            } catch (IOException e) {
+            } catch (FileNotFoundException e) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
+            } catch (Exception e) {
                 log.error("write excel error.", e);
                 throw new ServiceException(FileErrorCode.FILE_WRITE_ERROR);
             }
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
         } catch (IOException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
-        } catch (POIXMLException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
-        } catch (InvalidFormatException e) {
-            log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         }
     }
-
-    public void setExportCallback(ExportCallback callback) {
-        VerifyTools.requireNotBlank(callback, "callback");
-        this.callback = callback;
-    }
-
-    public ExportCallback getExportCallback() {
-        return this.callback;
-    }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
index 82bd3bd5936e4d5b51e0199bf24cd83ac10fe39f..14aabb1443f3a81155fec6e640946137081b302e 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XExcelParser.java
@@ -1,9 +1,12 @@
 package com.gitee.qdbp.tools.excel;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import org.apache.poi.POIXMLException;
-import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import java.net.URL;
+import java.util.Map;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
@@ -12,6 +15,7 @@ import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.enums.FileErrorCode;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.utils.ExcelHelper;
+import com.gitee.qdbp.tools.files.PathTools;
 
 /**
  * excel2007版解析器
@@ -29,43 +33,235 @@ public class XExcelParser {
         this.metadata = metadata;
     }
 
-    public void parse(InputStream is, ImportCallback cb) throws ServiceException {
-        try (Workbook wb = WorkbookFactory.create(is)) {
-            this.parse(wb, cb);
+    /**
+     * 导入Excel, 解析为JavaBean
+     *
+     * @param file Excel文件
+     * @param beanType JavaBean类型
+     * @param <T> 泛型
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public <T> ImportResult<T> parseAsBean(File file, Class<T> beanType) throws ServiceException {
+        try (InputStream input = new FileInputStream(file)) {
+            return parseAsBean(input, beanType);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(file.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails(file.getPath());
+        }
+    }
+
+    /**
+     * 导入Excel, 解析为JavaBean
+     *
+     * @param file Excel文件
+     * @param beanType JavaBean类型
+     * @param <T> 泛型
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public <T> ImportResult<T> parseAsBean(URL file, Class<T> beanType) throws ServiceException {
+        try (InputStream input = file.openStream()) {
+            return parseAsBean(input, beanType);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(file));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * 导入Excel, 解析为JavaBean
+     *
+     * @param input Excel文件输入流
+     * @param beanType JavaBean类型
+     * @param <T> 泛型
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public <T> ImportResult<T> parseAsBean(InputStream input, Class<T> beanType) throws ServiceException {
+        ImportAsBean<T> callback = new ImportAsBean<>(beanType);
+        parse(input, callback);
+        return new ImportResult<>(callback.getContents(), callback);
+    }
+
+    /**
+     * 导入Excel, 解析为JavaBean
+     *
+     * @param workbook Excel对象
+     * @param beanType JavaBean类型
+     * @param <T> 泛型
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public <T> ImportResult<T> parseAsBean(Workbook workbook, Class<T> beanType) throws ServiceException {
+        ImportAsBean<T> callback = new ImportAsBean<>(beanType);
+        parse(workbook, callback);
+        return new ImportResult<>(callback.getContents(), callback);
+    }
+
+    /**
+     * 导入Excel, 解析为Map数据
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public ImportResult<Map<String, Object>> parseAsMap(File file) throws ServiceException {
+        try (InputStream input = new FileInputStream(file)) {
+            return parseAsMap(input);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(file.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+    /**
+     * 导入Excel, 解析为Map数据
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public ImportResult<Map<String, Object>> parseAsMap(URL file) throws ServiceException {
+        try (InputStream input = file.openStream()) {
+            return parseAsMap(input);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(file));
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * 导入Excel, 解析为Map数据
+     *
+     * @param input Excel文件输入流
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public ImportResult<Map<String, Object>> parseAsMap(InputStream input) throws ServiceException {
+        ImportAsMap callback = new ImportAsMap();
+        parse(input, callback);
+        return new ImportResult<>(callback.getContents(), callback);
+    }
+
+    /**
+     * 导入Excel, 解析为Map数据
+     *
+     * @param workbook Excel对象
+     * @return 导入结果
+     * @throws ServiceException 解析失败
+     */
+    public ImportResult<Map<String, Object>> parseAsMap(Workbook workbook) throws ServiceException {
+        ImportAsMap callback = new ImportAsMap();
+        parse(workbook, callback);
+        return new ImportResult<>(callback.getContents(), callback);
+    }
+
+    /**
+     * 解析Excel内容
+     *
+     * @param file Excel文件
+     * @param callback 解析的回调处理类
+     * @throws ServiceException 解析失败
+     */
+    public void parse(File file, ImportCallback callback) throws ServiceException {
+        try (InputStream input = new FileInputStream(file)) {
+            parse(input, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(file.getPath());
+        } catch (IOException e) {
+            log.error("read excel error.", e);
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        }
+    }
+
+    /**
+     * 解析Excel内容
+     *
+     * @param file Excel文件
+     * @param callback 解析的回调处理类
+     * @throws ServiceException 解析失败
+     */
+    public void parse(URL file, ImportCallback callback) throws ServiceException {
+        try (InputStream input = file.openStream()) {
+            parse(input, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails(PathTools.toUriPath(file));
         } catch (IOException e) {
             log.error("read excel error.", e);
             throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
-        } catch (POIXMLException e) {
+        }
+    }
+
+    /**
+     * 解析Excel内容
+     *
+     * @param input Excel文件输入流
+     * @param callback 解析的回调处理类
+     * @throws ServiceException 解析失败
+     */
+    public void parse(InputStream input, ImportCallback callback) throws ServiceException {
+        try (Workbook workbook = WorkbookFactory.create(input)) {
+            this.parse(workbook, callback);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
+        } catch (IOException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
-        } catch (InvalidFormatException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         }
     }
 
-    public void parse(Workbook wb, ImportCallback cb) {
+    /**
+     * 解析Excel内容
+     *
+     * @param workbook Excel对象
+     * @param callback 解析的回调处理类
+     * @throws ServiceException 解析失败
+     */
+    public void parse(Workbook workbook, ImportCallback callback) {
         try {
-            cb.init(wb, metadata);
-            for (int i = 0, total = wb.getNumberOfSheets(); i < total; i++) {
-                if (metadata.isEnableSheet(i, wb.getSheetName(i))) {
-                    Sheet sheet = wb.getSheetAt(i);
-                    if (!cb.onSheetStart(sheet, metadata)) {
+            callback.init(workbook, metadata);
+            for (int i = 0, total = workbook.getNumberOfSheets(); i < total; i++) {
+                if (metadata.isEnableSheet(i, workbook.getSheetName(i))) {
+                    Sheet sheet = workbook.getSheetAt(i);
+                    if (!callback.onSheetStart(sheet, metadata)) {
                         continue; // 返回false跳过该Sheet 
                     }
 
                     // 解析Sheet
-                    ExcelHelper.parse(sheet, metadata, cb);
+                    ExcelHelper.parse(sheet, metadata, callback);
 
-                    if (!cb.onSheetFinished(sheet, metadata)) {
+                    if (!callback.onSheetFinished(sheet, metadata)) {
                         break; // 返回false跳过后面的所有Sheet 
                     }
                 }
             }
-            cb.finish(wb, metadata);
-        } catch (POIXMLException e) {
+            callback.finish(workbook, metadata);
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         }
     }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
index c4922d501f726d7eeac48045a3b348431ec62cee..b2757023258dc6bad826c5d97b56fbe7434880df 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/XMetadata.java
@@ -7,6 +7,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import org.apache.poi.ss.usermodel.Row;
 import com.gitee.qdbp.tools.excel.condition.IndexListCondition;
 import com.gitee.qdbp.tools.excel.condition.IndexRangeCondition;
 import com.gitee.qdbp.tools.excel.condition.MatchesRowCondition;
@@ -17,10 +18,9 @@ import com.gitee.qdbp.tools.excel.rule.CellRule;
 import com.gitee.qdbp.tools.excel.utils.MetadataTools;
 import com.gitee.qdbp.tools.utils.Config;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Row;
 
 /**
- * excel配置数据<br>
+ * Excel配置数据<br>
  * 有哪些配置项详见{@link MetadataTools#parseProperties(Properties)}<br>
  *
  * @author zhaohuihua
@@ -52,6 +52,8 @@ public class XMetadata implements Serializable {
     private IndexRangeCondition footerRows;
     /** Sheet名称填充至哪个字段 **/
     private String sheetNameFillTo;
+    /** 行序号填充至哪个字段 **/
+    private String rowIndexFillTo;
     /** Sheet序号配置, 默认读取第1个Sheet **/
     private IndexListCondition sheetIndexs = new IndexListCondition(0);
     /** Sheet名称配置, 默认全部匹配 **/
@@ -67,7 +69,7 @@ public class XMetadata implements Serializable {
 
     /**
      * 构造函数
-     * 
+     *
      * @param config 配置项
      * @deprecated 改为{@link MetadataTools#parseProperties(Properties)}
      */
@@ -82,6 +84,7 @@ public class XMetadata implements Serializable {
         this.setHeaderRows(metadata.getHeaderRows()); // 表头所在的行号
         this.setFooterRows(metadata.getFooterRows()); // 页脚所在的行号
         this.setSheetNameFillTo(metadata.getSheetNameFillTo()); // Sheet名称填充至哪个字段
+        this.setRowIndexFillTo(metadata.getRowIndexFillTo()); // 行序号填充至哪个字段
         this.setSheetIndexs(metadata.getSheetIndexs()); // Sheet序号配置
         this.setSheetNames(metadata.getSheetNames()); // Sheet名称配置
     }
@@ -233,6 +236,16 @@ public class XMetadata implements Serializable {
         this.sheetNameFillTo = sheetNameFillTo;
     }
 
+    /** 行序号填充至哪个字段 **/
+    public String getRowIndexFillTo() {
+        return rowIndexFillTo;
+    }
+
+    /** 行序号填充至哪个字段 **/
+    public void setRowIndexFillTo(String rowIndexFillTo) {
+        this.rowIndexFillTo = rowIndexFillTo;
+    }
+
     /** 字段与转换规则的映射表 **/
     public Map<String, List<CellRule>> getRules() {
         return rules;
@@ -244,11 +257,20 @@ public class XMetadata implements Serializable {
     }
 
     /** 增加转换规则 **/
-    public void addRule(String column, List<CellRule> rule) {
+    public void addRule(String column, CellRule rule) {
+        addRule(column, Collections.singletonList(rule));
+    }
+
+    /** 增加转换规则 **/
+    public void addRule(String column, List<CellRule> rules) {
         if (this.rules == null) {
             this.rules = new HashMap<>();
+            this.rules.put(column, new ArrayList<>(rules));
+        } else if (this.rules.containsKey(column)) {
+            this.rules.get(column).addAll(rules);
+        } else {
+            this.rules.put(column, new ArrayList<>(rules));
         }
-        this.rules.put(column, rule);
     }
 
     /** 获取指定列的转换规则 **/
@@ -299,10 +321,27 @@ public class XMetadata implements Serializable {
         instance.setHeaderRows(this.getHeaderRows()); // 表头所在的行号
         instance.setFooterRows(this.getFooterRows()); // 页脚所在的行号
         instance.setSheetNameFillTo(this.getSheetNameFillTo()); // Sheet名称填充至哪个字段
+        instance.setRowIndexFillTo(this.getRowIndexFillTo()); // 行序号填充至哪个字段
         instance.setSheetIndexs(this.getSheetIndexs()); // Sheet序号配置
         instance.setSheetNames(this.getSheetNames()); // Sheet名称配置
         instance.setCopyConcatFields(this.getCopyConcatFields()); // 字段复制合并参数 
         return instance;
     }
 
+    /**
+     * 创建最简单的XMetadata
+     *
+     * @param skipRows 跳过几行, skip.rows = 5
+     * @param fieldNames 字段名配置, field.names = *id|*name|positive|height||birthday|gender|subsidy
+     * @return XMetadata
+     */
+    public static XMetadata of(int skipRows, String fieldNames) {
+        XMetadata instance = new XMetadata();
+        instance.setSkipRows(skipRows);
+        instance.setFieldInfos(MetadataTools.parseFieldInfoByText(fieldNames));
+        instance.setSheetNames(new NameListCondition());
+        instance.setSheetNameFillTo("sheetName");
+        instance.setRowIndexFillTo("rowIndex");
+        return instance;
+    }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexListCondition.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexListCondition.java
index a818c7bd6ae58b6d8bf096667c39dd0c02879178..1d2c2b5c55a64efd3d0046c139b33438d89a46b1 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexListCondition.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexListCondition.java
@@ -3,13 +3,11 @@ package com.gitee.qdbp.tools.excel.condition;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 序号列表配置类<br>
@@ -29,12 +27,6 @@ public class IndexListCondition implements Serializable {
     /** 日志对象 **/
     private static final Logger log = LoggerFactory.getLogger(IndexListCondition.class);
 
-    /** 以!开头的是排除法配置 **/
-    private static final Pattern EXCLUDE = Pattern.compile("^\\s*!\\s*");
-
-    /** 开始序号至结束序号 **/
-    private static final Pattern BETWEEN = Pattern.compile("^\\s*(\\d+)\\s*-\\s*(\\d+)\\s*$");
-
     /** 是不是全部允许 **/
     private boolean all = false;
 
@@ -84,46 +76,47 @@ public class IndexListCondition implements Serializable {
     }
 
     private void parse(String text, int startBy) {
-
-        if (VerifyTools.isBlank(text) || VerifyTools.isBlank(text.trim())) {
+        if (VerifyTools.isBlank(text)) {
             return;
         }
-
-        if ("*".equals(text.trim())) {
+        text = text.trim();
+        if (VerifyTools.isBlank(text)) {
+            return;
+        }
+        if ("*".equals(text)) {
             this.all = true;
             return;
         }
 
-        Matcher em = EXCLUDE.matcher(text);
-        if (em.find()) {
+        // 以感叹号开头的, 是排除规则
+        if (text.charAt(0) == '!') {
             exclude = true;
-            text = em.replaceFirst("").trim();
+            text = text.substring(1).trim();
         }
         String[] digits = StringTools.split(text);
-        List<Integer> indexs = new ArrayList<>();
+        List<Integer> indexes = new ArrayList<>();
         for (String digit : digits) {
-            digit = digit.trim();
             if (StringTools.isDigit(digit)) {
-                indexs.add(ConvertTools.toInteger(digit) - startBy);
-            } else {
-                Matcher bm = BETWEEN.matcher(digit);
-                if (bm.matches()) {
-                    int start = ConvertTools.toInteger(bm.group(1)) - startBy;
-                    int end = ConvertTools.toInteger(bm.group(2)) - startBy;
-                    if (start > end) {
-                        int temp = start;
-                        start = end;
-                        end = temp;
-                    }
-                    for (int i = start; i <= end; i++) {
-                        indexs.add(i);
-                    }
-                } else {
-                    log.warn("ExcelIndexError: " + digit);
-                }
+                indexes.add(ConvertTools.toInteger(digit) - startBy);
+                continue;
+            }
+            String[] ranges = StringTools.split(digit, '-');
+            if (ranges.length != 2 || !StringTools.isDigit(ranges[0]) || !StringTools.isDigit(ranges[1])) {
+                log.warn("ExcelIndexError: " + digit);
+                continue;
+            }
+            int start = ConvertTools.toInteger(ranges[0]) - startBy;
+            int end = ConvertTools.toInteger(ranges[1]) - startBy;
+            if (start > end) {
+                int temp = start;
+                start = end;
+                end = temp;
+            }
+            for (int i = start; i <= end; i++) {
+                indexes.add(i);
             }
         }
-        this.indexs = indexs;
+        this.indexs = indexes;
     }
 
     public String toString() {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexRangeCondition.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexRangeCondition.java
index 7b187a42fd48e6dd3d0733f7d2f1e2649c1f7e5d..00e37c6f532b162c63f79b74d803a14d77b773aa 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexRangeCondition.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/IndexRangeCondition.java
@@ -3,13 +3,11 @@ package com.gitee.qdbp.tools.excel.condition;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 序号范围配置类<br>
@@ -26,9 +24,6 @@ public class IndexRangeCondition implements Serializable {
     /** 日志对象 **/
     private static final Logger log = LoggerFactory.getLogger(IndexRangeCondition.class);
 
-    /** 开始序号至结束序号 **/
-    private static final Pattern BETWEEN = Pattern.compile("^\\s*(\\d+)\\s*-\\s*(\\d+)\\s*$");
-
     /** Index列表 **/
     private List<Integer> indexs;
 
@@ -72,33 +67,34 @@ public class IndexRangeCondition implements Serializable {
     }
 
     private void parse(String text, int startBy) {
-
-        if (VerifyTools.isBlank(text) || VerifyTools.isBlank(text.trim())) {
+        if (VerifyTools.isBlank(text)) {
+            return;
+        }
+        text = text.trim();
+        if (VerifyTools.isBlank(text)) {
             return;
         }
-
         String[] digits = StringTools.split(text);
         this.indexs = new ArrayList<>();
         for (String digit : digits) {
-            digit = digit.trim();
             if (StringTools.isDigit(digit)) {
                 addIndex(ConvertTools.toInteger(digit) - startBy);
-            } else {
-                Matcher bm = BETWEEN.matcher(digit);
-                if (bm.matches()) {
-                    int start = ConvertTools.toInteger(bm.group(1)) - startBy;
-                    int end = ConvertTools.toInteger(bm.group(2)) - startBy;
-                    if (start > end) {
-                        int temp = start;
-                        start = end;
-                        end = temp;
-                    }
-                    for (int i = start; i <= end; i++) {
-                        addIndex(i);
-                    }
-                } else {
-                    log.warn("ExcelIndexError: " + digit);
-                }
+                continue;
+            }
+            String[] ranges = StringTools.split(digit, '-');
+            if (ranges.length != 2 || !StringTools.isDigit(ranges[0]) || !StringTools.isDigit(ranges[1])) {
+                log.warn("ExcelIndexError: " + digit);
+                continue;
+            }
+            int start = ConvertTools.toInteger(ranges[0]) - startBy;
+            int end = ConvertTools.toInteger(ranges[1]) - startBy;
+            if (start > end) {
+                int temp = start;
+                start = end;
+                end = temp;
+            }
+            for (int i = start; i <= end; i++) {
+                addIndex(i);
             }
         }
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/NameListCondition.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/NameListCondition.java
index c3466db539f34be67482aee7b7ae21c1af6b1790..c778a458f19f84b10dc6cf7efcd03236c012e7f5 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/NameListCondition.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/NameListCondition.java
@@ -4,8 +4,6 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
@@ -24,9 +22,6 @@ public class NameListCondition implements Serializable {
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
 
-    /** 以!开头的是排除法配置 **/
-    private static final Pattern EXCLUDE = Pattern.compile("^\\s*!\\s*");
-
     /** 是不是全部允许 **/
     private boolean all = false;
 
@@ -65,8 +60,11 @@ public class NameListCondition implements Serializable {
     }
 
     private void parse(String text) {
-
-        if (VerifyTools.isBlank(text) || VerifyTools.isBlank(text.trim())) {
+        if (VerifyTools.isBlank(text)) {
+            return;
+        }
+        text = text.trim();
+        if (VerifyTools.isBlank(text)) {
             return;
         }
 
@@ -75,10 +73,10 @@ public class NameListCondition implements Serializable {
             return;
         }
 
-        Matcher em = EXCLUDE.matcher(text);
-        if (em.find()) {
+        // 以感叹号开头的, 是排除规则
+        if (text.charAt(0) == '!') {
             exclude = true;
-            text = em.replaceFirst("").trim();
+            text = text.substring(1).trim();
         }
 
         String[] digits = StringTools.split(text);
@@ -89,7 +87,6 @@ public class NameListCondition implements Serializable {
         this.names = names;
     }
 
-
     public String toString() {
         if (all) {
             return "*";
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/Required.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/Required.java
index 0c114504360c23c30b88b45046f7736b415908cf..d049d683817790894fd5caa32547e3cdee980434 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/Required.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/condition/Required.java
@@ -1,8 +1,6 @@
 package com.gitee.qdbp.tools.excel.condition;
 
 import java.io.Serializable;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -16,9 +14,6 @@ public class Required implements Serializable {
     /** 版本序列号 **/
     private static final long serialVersionUID = 1L;
 
-    /** 星号开头或(*)结尾的字段为必填字段: [* 姓名] or [姓名 (*)] **/
-    private static final Pattern REQUIRED = Pattern.compile("(^\\s*\\*\\s*|\\s*\\(\\*\\)\\s*$)");
-
     /** 字段名 **/
     private final String name;
 
@@ -43,14 +38,20 @@ public class Required implements Serializable {
     public static Required of(String text) {
         VerifyTools.requireNotBlank(text, "text");
 
-        String string = text.trim();
         // 星号开头或(*)结尾的字段为必填字段
-        Matcher matcher = REQUIRED.matcher(string);
-        if (matcher.find()) {
-            return new Required(matcher.replaceAll(""), true);
-        } else {
-            return new Required(string, false);
+        // * 姓名
+        // 姓名 (*)
+        text = text.trim();
+        if (text.charAt(0) == '*') {
+            String field = text.substring(1);
+            return new Required(field.trim(), true);
+        }
+        String suffix = "(*)";
+        if (text.endsWith(suffix)) {
+            String field = text.substring(0, text.length() - suffix.length());
+            return new Required(field.trim(), true);
         }
+        return new Required(text, false);
     }
 
     public String toString() {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanContainer.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanContainer.java
index 121ef82076688beba708861ca5019cf761d0603d..c74353aed840e133db4779722bbd6dd0dfa03722 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanContainer.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanContainer.java
@@ -4,7 +4,6 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.tools.excel.exception.ResultSetMismatchException;
 import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.StringTools;
@@ -130,7 +129,7 @@ public class BeanContainer implements Serializable {
 
         List<T> temp = new ArrayList<>();
         for (Object object : list) {
-            temp.add(TypeUtils.castToJavaBean(object, type));
+            temp.add(JsonTools.convert(object, type));
         }
         return temp;
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanGroup.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanGroup.java
index 1c87cdf444c0576a829a17baeb869c9131e4e1a7..0a63c292a6c658f50a6affdac4182c5b2b247b11 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanGroup.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/BeanGroup.java
@@ -7,7 +7,6 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.tools.excel.exception.ResultSetMismatchException;
 import com.gitee.qdbp.tools.excel.model.ColumnInfo;
 import com.gitee.qdbp.tools.utils.ConvertTools;
@@ -450,8 +449,8 @@ public class BeanGroup implements Serializable {
 
     private static boolean compareNumber(Object expectValue, Object actualValue) {
         try {
-            Double expectNumber = TypeUtils.castToDouble(expectValue);
-            Double actualNumber = TypeUtils.castToDouble(actualValue);
+            Double expectNumber = JsonTools.convert(expectValue, Double.class);
+            Double actualNumber = JsonTools.convert(actualValue, Double.class);
             return expectNumber.doubleValue() == actualNumber.doubleValue();
         } catch (Exception e) {
             return compareString(expectValue, actualValue);
@@ -460,8 +459,8 @@ public class BeanGroup implements Serializable {
 
     private static boolean compareDate(Object expectValue, Object actualValue) {
         try {
-            Date expectDate = TypeUtils.castToDate(expectValue);
-            Date actualDate = TypeUtils.castToDate(actualValue);
+            Date expectDate = JsonTools.convert(expectValue, Date.class);
+            Date actualDate = JsonTools.convert(actualValue, Date.class);
             return expectDate.getTime() == actualDate.getTime();
         } catch (Exception e) {
             return compareString(expectValue, actualValue);
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelBeans.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelBeans.java
index e5647d62a8bc2b8710920a9bba772bc2e3e04a5a..01940b431ec23f4dfab23c2f39c9256cd182c9ff 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelBeans.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelBeans.java
@@ -1,5 +1,6 @@
 package com.gitee.qdbp.tools.excel.json;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -12,8 +13,6 @@ import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.apache.poi.POIXMLException;
-import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
@@ -120,12 +119,9 @@ public class ExcelBeans {
         } catch (IOException e) {
             log.error("read excel error.", e);
             throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
-        } catch (POIXMLException e) {
+        } catch (Exception e) {
             log.error("read excel error.", e);
             throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
-        } catch (InvalidFormatException e) {
-            log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
         } finally {
             this.lock.unlock();
         }
@@ -144,7 +140,7 @@ public class ExcelBeans {
             this.init();
             this.doParseSheet(wb, sheetName, 0);
             return this.container;
-        } catch (POIXMLException e) {
+        } catch (Exception e) {
             log.error("read sheet error, sheetName=" + sheetName, e);
             throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
         } finally {
@@ -163,9 +159,15 @@ public class ExcelBeans {
             this.init();
             this.doParseSheet(sheet, 0);
             return this.container;
-        } catch (POIXMLException e) {
-            log.error("read sheet error, sheetName=" + sheet.getSheetName(), e);
-            throw new ServiceException(FileErrorCode.FILE_TEMPLATE_ERROR);
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
+            log.error("read excel error.", e);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         } finally {
             this.lock.unlock();
         }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelToJson.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelToJson.java
index 346bee345211d1e710d99183380576e1fc294ed1..c45034a1520a8aad76da5d500bf9b3c38cccd39a 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelToJson.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/json/ExcelToJson.java
@@ -11,7 +11,6 @@ import java.util.List;
 import java.util.Map;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import com.alibaba.fastjson.JSONException;
 import com.alibaba.fastjson.serializer.JSONSerializer;
 import com.alibaba.fastjson.serializer.SerializeConfig;
 import com.alibaba.fastjson.serializer.SerializeWriter;
@@ -19,7 +18,6 @@ import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.alibaba.fastjson.serializer.ToStringSerializer;
 import com.gitee.qdbp.able.enums.FileErrorCode;
 import com.gitee.qdbp.able.exception.ServiceException;
-import com.gitee.qdbp.tools.excel.ExcelErrorCode;
 import com.gitee.qdbp.tools.excel.ImportCallback;
 import com.gitee.qdbp.tools.excel.XExcelParser;
 import com.gitee.qdbp.tools.excel.XMetadata;
@@ -229,11 +227,8 @@ public class ExcelToJson {
 
         @Override
         public void callback(Map<String, Object> map, RowInfo row) throws ServiceException {
-            try {
-                rows.add(map);
-            } catch (JSONException e) {
-                throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
-            }
+            // throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
+            rows.add(map);
         }
 
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/model/FailedInfo.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/model/FailedInfo.java
index e5ac9108e1827d15b8106b1e0128b572d90bb495..317166763fc08efe5a6100c02ace31cb51644470 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/model/FailedInfo.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/model/FailedInfo.java
@@ -2,6 +2,7 @@ package com.gitee.qdbp.tools.excel.model;
 
 import java.io.Serializable;
 import com.gitee.qdbp.able.result.IBatchResult.Failed;
+import com.gitee.qdbp.able.result.IResultException;
 import com.gitee.qdbp.able.result.IResultMessage;
 
 /**
@@ -34,7 +35,6 @@ public class FailedInfo implements Failed, Serializable {
     private String message;
 
     public FailedInfo() {
-
     }
 
     public FailedInfo(String sheetName, Integer index, IResultMessage result) {
@@ -47,7 +47,11 @@ public class FailedInfo implements Failed, Serializable {
         this.field = field;
         this.value = value;
         this.code = result.getCode();
-        this.message = result.getMessage();
+        if (result instanceof IResultException) {
+            this.message = ((IResultException) result).getSimpleMessage();
+        } else {
+            this.message = result.getMessage();
+        }
     }
 
     /** 获取Sheet页签的名称 **/
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/DateRule.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/DateRule.java
index a517aae255cdfdb65ed4509c9a26d58477781e48..6209d844ee6240f3158ac78181652bee0a0a3221 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/DateRule.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/DateRule.java
@@ -5,10 +5,10 @@ import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -54,7 +54,7 @@ public class DateRule implements CellRule, Serializable {
         } else if (cellInfo.getValue() instanceof Date) {
             cellInfo.setValue(new SimpleDateFormat(pattern).format((Date) cellInfo.getValue()));
         } else {
-            Date date = TypeUtils.castToDate(cellInfo.getValue());
+            Date date = JsonTools.convert(cellInfo.getValue(), Date.class);
             cellInfo.setValue(new SimpleDateFormat(pattern).format(date));
         }
         return null;
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/MapRule.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/MapRule.java
index be63485887d565c8cd0280550c9a4c23cdee2e87..1d4d241ea9702c3258a0ff69d397b598dd4c358d 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/MapRule.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/MapRule.java
@@ -4,11 +4,11 @@ import java.io.Serializable;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
-import com.alibaba.fastjson.JSON;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
 import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
@@ -33,7 +33,7 @@ public class MapRule implements CellRule, Serializable {
      * @param rule 映射规则 如 { "PROVINCE":"1|省", "CITY":"2|市", "DISTRICT":"3|区|县|区/县" }
      */
     public MapRule(String rule) {
-        this(JSON.parseObject(rule));
+        this(JsonTools.parseAsMap(rule));
     }
 
     /**
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/NumberRule.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/NumberRule.java
index 7666cc78554144bf84df599abad2d949a699dd7e..522d6dd897b7c8337e9809eb168cb13e3ea12d54 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/NumberRule.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/NumberRule.java
@@ -2,9 +2,9 @@ package com.gitee.qdbp.tools.excel.rule;
 
 import java.io.Serializable;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
@@ -48,17 +48,17 @@ public class NumberRule implements CellRule, Serializable {
             type = "double";
         }
         if (type.equalsIgnoreCase("int") || type.equalsIgnoreCase("integer")) {
-            return TypeUtils.castToInt(value);
+            return JsonTools.convert(value, Integer.class);
         } else if (type.equalsIgnoreCase("float")) {
-            return TypeUtils.castToFloat(value);
+            return JsonTools.convert(value, Float.class);
         } else if (type.equalsIgnoreCase("long")) {
-            return TypeUtils.castToLong(value);
+            return JsonTools.convert(value, Long.class);
         } else if (type.equalsIgnoreCase("short")) {
-            return TypeUtils.castToShort(value);
+            return JsonTools.convert(value, Short.class);
         } else if (type.equalsIgnoreCase("byte")) {
-            return TypeUtils.castToByte(value);
+            return JsonTools.convert(value, Byte.class);
         } else {
-            return TypeUtils.castToDouble(value);
+            return JsonTools.convert(value, Double.class);
         }
     }
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RateRule.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RateRule.java
index fe26df8854c4d3f2b38ed198aef869cc09924871..5f34265786afe024b21b48ee17e57c07364d049d 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RateRule.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RateRule.java
@@ -2,10 +2,10 @@ package com.gitee.qdbp.tools.excel.rule;
 
 import java.io.Serializable;
 import java.util.Map;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.model.CellInfo;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 
 public class RateRule implements CellRule, Serializable {
@@ -30,7 +30,7 @@ public class RateRule implements CellRule, Serializable {
             return null;
         }
         if (value instanceof String) {
-            Double number = TypeUtils.castToDouble(value);
+            Double number = JsonTools.convert(value, Double.class);
             cellInfo.setValue(number == null ? null : number * rate);
         } else if (value instanceof Number) {
             Number number = (Number) value;
@@ -50,7 +50,7 @@ public class RateRule implements CellRule, Serializable {
         if (value instanceof Number) {
             cellInfo.setValue(((Number) value).doubleValue() / rate);
         } else {
-            Double number = TypeUtils.castToDouble(value);
+            Double number = JsonTools.convert(value, Double.class);
             cellInfo.setValue(number == null ? null : number / rate);
         }
         return null;
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RuleFactory.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RuleFactory.java
index 83b1fefab52adc55da1de0b582017d1a8e0d9723..3624c0b8ac25a906df85bca67572a5c1c675ad15 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RuleFactory.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/rule/RuleFactory.java
@@ -3,12 +3,11 @@ package com.gitee.qdbp.tools.excel.rule;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Pattern;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.tools.utils.ConvertTools;
 import com.gitee.qdbp.tools.utils.DateTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
  * 规则注册工厂
@@ -60,7 +59,7 @@ public class RuleFactory {
                     return null;
                 }
                 try {
-                    Boolean value = TypeUtils.castToBoolean(options);
+                    Boolean value = JsonTools.convert(options, Boolean.class);
                     return value ? new IgnoreIllegalValue() : null;
                 } catch (Exception e) {
                     throw new IllegalArgumentException(illegalArgumentType("ignoreIllegalValue", options));
@@ -147,8 +146,8 @@ public class RuleFactory {
                     throw new IllegalArgumentException(illegalArgumentType("map", options));
                 }
                 String string = (String) options;
-                if (string.startsWith("{")) {
-                    JSONObject json = (JSONObject) JSON.parse(string);
+                if (VerifyTools.isJsonObjectString(string)) {
+                    Map<String, Object> json = JsonTools.parseAsMap(string);
                     return new MapRule(json);
                 } else {
                     // 0:未知, 1:男, 2:女
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
index 00dd25e90e2eaed1faed47385bbc31fc2c0c1a2d..aea1e95426931dac2c428442d5377203514377e8 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelHelper.java
@@ -4,7 +4,12 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.Pattern;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Row.MissingCellPolicy;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.excel.SheetFillCallback;
@@ -17,12 +22,6 @@ import com.gitee.qdbp.tools.excel.model.FieldInfo;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Cell;
-import org.apache.poi.ss.usermodel.Row;
-import org.apache.poi.ss.usermodel.Row.MissingCellPolicy;
-import org.apache.poi.ss.usermodel.Sheet;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Excel处理类
@@ -38,8 +37,6 @@ public class ExcelHelper {
 
     private static final Logger log = LoggerFactory.getLogger(ExcelHelper.class);
 
-    private static final Pattern IGNORE_SHEET_NAME = Pattern.compile("sheet\\d*", Pattern.CASE_INSENSITIVE);
-
     public static void parse(Sheet sheet, XMetadata metadata, SheetParseCallback cb) {
 
         String sheetName = sheet.getSheetName();
@@ -74,11 +71,7 @@ public class ExcelHelper {
                 parse(sheetName, row, i + 1, columnInfos, metadata, cb);
             } catch (ServiceException e) {
                 cb.addFailed(sheetName, i + 1, e.getDetails(), null, e);
-                if (e.getCause() != null) {
-                    String error = e.getMessage();
-                    String cause = e.getCause().getMessage();
-                    log.error("Sheet[{}], excel parse error, {}, {}", sheetName, error, cause, e);
-                }
+                log.error("Sheet[{}], excel parse error, {}", sheetName, e.getMessage());
             } catch (NumberFormatException e) {
                 cb.addFailed(sheetName, i + 1, ResultCode.PARAMETER_FORMAT_ERROR);
             } catch (IllegalArgumentException e) {
@@ -117,15 +110,19 @@ public class ExcelHelper {
 
         cb.addTotal(1); // 总行数加1
 
+        // 生成单元格信息
+        Map<String, Object> data = new HashMap<>();
         // Sheet名称填充至指定字段
         if (VerifyTools.isNotBlank(metadata.getSheetNameFillTo())) {
-            if (!IGNORE_SHEET_NAME.matcher(sheetName).matches()) {
-                map.put(metadata.getSheetNameFillTo(), sheetName);
+            if (!isIgnoreSheetName(sheetName)) {
+                data.put(metadata.getSheetNameFillTo(), sheetName);
             }
         }
-
-        // 生成单元格信息
-        Map<String, Object> data = new HashMap<>();
+        // 行序号填充至哪个字段
+        if (VerifyTools.isNotBlank(metadata.getRowIndexFillTo())) {
+            data.put(metadata.getRowIndexFillTo(), rowIndex);
+        }
+        // 生成CellInfo列表
         List<CellInfo> cellInfos = newCellInfos(columnInfos, metadata);
         for (CellInfo info : cellInfos) {
             if (info == null) {
@@ -138,24 +135,32 @@ public class ExcelHelper {
                 // 调用转换规则
                 cb.convert(info, data);
             } catch (ServiceException e) {
-                // 拼接[列序号]字段名(单元格的值), 如[D:年龄]100Kg
-                String title = '[' + info.getTitle() + ']';
+                String title = info.getTitle();
+                // 拼接单元格的值, 如[D3]100Kg
+                StringBuilder text = new StringBuilder();
                 if (info.getColumn() != null) {
-                    title = '[' + ExcelTools.columnIndexToName(info.getColumn()) + ':' + info.getTitle() + ']';
+                    String columnIndex = ExcelTools.columnIndexToName(info.getColumn());
+                    text.append('[').append(columnIndex).append(info.getRow()).append(']');
                 }
                 if (VerifyTools.isNotBlank(original)) {
                     // 不知道怎么取Excel单元格的原始文本, 因此日期,时间就不好提示了
                     if (original instanceof String || original instanceof Boolean || original instanceof Number) {
-                        title += StringTools.ellipsis(original.toString(), 30);
+                        text.append(StringTools.ellipsis(original.toString(), 30));
                     }
                 }
-                cb.addFailed(sheetName, rowIndex, title, original, e);
+                cb.addFailed(sheetName, rowIndex, title, text.toString(), e);
                 return;
             }
             { // 检查必填字段
                 Object value = info.getValue();
                 if (info.isRequired() && VerifyTools.isBlank(value)) {
-                    cb.addFailed(sheetName, rowIndex, info.getTitle(), value, ResultCode.PARAMETER_IS_REQUIRED);
+                    String title = info.getTitle();
+                    StringBuilder text = new StringBuilder();
+                    if (info.getColumn() != null) {
+                        String columnIndex = ExcelTools.columnIndexToName(info.getColumn());
+                        text.append('[').append(columnIndex).append(info.getRow()).append(']');
+                    }
+                    cb.addFailed(sheetName, rowIndex, title, text.toString(), ResultCode.PARAMETER_IS_REQUIRED);
                     return;
                 }
             }
@@ -173,6 +178,16 @@ public class ExcelHelper {
         cb.callback(data, rowInfo);
     }
 
+    private static boolean isIgnoreSheetName(String string) {
+        if (string.startsWith("Sheet") || string.startsWith("sheet")) {
+            String suffix = string.substring(5).trim();
+            if (suffix.length() == 0 || StringTools.isDigit(suffix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public static void fill(List<?> list, Sheet sheet, XMetadata metadata, SheetFillCallback cb) {
 
         String sheetName = sheet.getSheetName();
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
index 00a14873b9f3dacd69191ed418372a0860cb607d..786997fd6f68ef95eac853ee07db164a5b0dd556 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/ExcelTools.java
@@ -7,13 +7,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import com.alibaba.fastjson.util.TypeUtils;
-import com.gitee.qdbp.tools.excel.model.CellInfo;
-import com.gitee.qdbp.tools.excel.model.CopyConcat;
-import com.gitee.qdbp.tools.excel.rule.CellRule;
-import com.gitee.qdbp.tools.utils.ConvertTools;
-import com.gitee.qdbp.tools.utils.JsonTools;
-import com.gitee.qdbp.tools.utils.VerifyTools;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.CellStyle;
 import org.apache.poi.ss.usermodel.CellType;
@@ -28,6 +21,12 @@ import org.apache.poi.ss.util.CellRangeAddress;
 import org.apache.poi.ss.util.PaneInformation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import com.gitee.qdbp.tools.excel.model.CellInfo;
+import com.gitee.qdbp.tools.excel.model.CopyConcat;
+import com.gitee.qdbp.tools.excel.rule.CellRule;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
  * Excel工具类
@@ -265,25 +264,26 @@ public abstract class ExcelTools {
 
         // 单元格类型
         CellType cellType = src.getCellTypeEnum();
-        target.setCellType(cellType);
 
         if (cellType == CellType.FORMULA) { // 公式
             target.setCellFormula(src.getCellFormula());
-        } else if (copyValue) { // 复制内容
-
-            if (cellType == CellType.ERROR) { // 错误
-                target.setCellErrorValue(src.getErrorCellValue());
-            } else {
-                Object value = getCellValue(src);
-                setCellValue(target, value);
-            }
+        } else {
+            target.setCellType(cellType);
+            if (copyValue) { // 复制内容
+                if (cellType == CellType.ERROR) { // 错误
+                    target.setCellErrorValue(src.getErrorCellValue());
+                } else {
+                    Object value = getCellValue(src);
+                    setCellValue(target, value);
+                }
 
-            // 评论
-            Comment comment = src.getCellComment();
-            if (comment == null) {
-                target.removeCellComment();
-            } else {
-                target.setCellComment(comment);
+                // 评论
+                Comment comment = src.getCellComment();
+                if (comment == null) {
+                    target.removeCellComment();
+                } else {
+                    target.setCellComment(comment);
+                }
             }
         }
     }
@@ -478,16 +478,16 @@ public abstract class ExcelTools {
             } else if (value instanceof Calendar) {
                 cell.setCellValue((Calendar) value);
             } else if (value instanceof Boolean) {
-                cell.setCellValue(TypeUtils.castToBoolean(value));
+                cell.setCellValue(JsonTools.convert(value, Boolean.class));
             } else if (value instanceof Number) {
                 // Cell.setCellValue()对数字的处理只有double类型
                 cell.setCellValue(((Number) value).doubleValue());
             } else {
-                cell.setCellValue(TypeUtils.castToString(value));
+                cell.setCellValue(value.toString());
             }
             break;
         case BOOLEAN:
-            cell.setCellValue(TypeUtils.castToBoolean(value));
+            cell.setCellValue(JsonTools.convert(value, Boolean.class));
             break;
         case NUMERIC:
             switch (cell.getCellStyle().getDataFormat()) {
@@ -498,19 +498,19 @@ public abstract class ExcelTools {
             case 21: // HH:mm:ss
             case 22: // yyyy-MM-dd HH:mm:ss
             case 32: // h时mm分
-                cell.setCellValue(TypeUtils.castToDate(value));
+                cell.setCellValue(JsonTools.convert(value, Date.class));
                 break;
             default:
                 if (DateUtil.isCellDateFormatted(cell)) { // 日期时间
-                    cell.setCellValue(TypeUtils.castToDate(value));
+                    cell.setCellValue(JsonTools.convert(value, Date.class));
                 } else { // 数字
-                    cell.setCellValue(TypeUtils.castToDouble(value));
+                    cell.setCellValue(JsonTools.convert(value, Double.class));
                 }
                 break;
             }
             break;
         default: // STRING and other
-            cell.setCellValue(TypeUtils.castToString(value));
+            cell.setCellValue(value.toString());
             break;
         }
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
index 43bf02ab160ae24e071543862e0a9533237891d1..c187bd34a6ad13fc774ff58b46198daf25393e7b 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/excel/utils/MetadataTools.java
@@ -1,5 +1,6 @@
 package com.gitee.qdbp.tools.excel.utils;
 
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -8,9 +9,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.ImportCallback;
 import com.gitee.qdbp.tools.excel.XExcelParser;
@@ -34,12 +38,6 @@ import com.gitee.qdbp.tools.utils.JsonTools;
 import com.gitee.qdbp.tools.utils.PropertyTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
-import org.apache.poi.ss.usermodel.Cell;
-import org.apache.poi.ss.usermodel.Row;
-import org.apache.poi.ss.usermodel.Sheet;
-import org.apache.poi.ss.usermodel.Workbook;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 元数据解析工具类
@@ -55,17 +53,28 @@ public class MetadataTools {
 
     private static final Logger log = LoggerFactory.getLogger(MetadataTools.class);
 
+    /**
+     * 解析XMetadata<br>
+     *
+     * @param url 配置文件路径
+     * @return XMetadata
+     * @see #parseProperties(Properties)
+     */
+    public static XMetadata parseProperties(URL url) {
+        return parseProperties(PropertyTools.load(url, "UTF-8"));
+    }
+
     /**
      * 解析XMetadata<br>
      * MetadataTools.parseProperties(PropertyTools.load(filePath)); <pre>
      * ## SheetIndex从1开始, 行号从1开始, 列号从A开始
-     * 
+     *
      * ## 字段对应关系, field.names/field.rows必填其一
      * ## 字段列表
      * # field.names = *id|*name|positive|height||birthday|gender|subsidy
      * ## 字段行
      * field.rows = 5
-     * 
+     *
      * ## 跳过的行数(不配置时默认取字段行或标题行的下一行)
      * # skip.rows = 2
      * ## 标题行号,从1开始
@@ -77,7 +86,7 @@ public class MetadataTools {
      * ## 包含指定关键字时跳过此行
      * ## A列为空, 或B列包含小计且H列包含元, 或B列包含总计且H列包含元
      * # skip.row.when.contains = { A:"NULL" }, { B:"小计", H:"元" }, { B:"总计", H:"元" }
-     * 
+     *
      * ## 加载哪些Sheet, sheet.index/sheet.name必填其一
      * ## 配置规则: * 表示全部
      * ## 配置规则: 1|2|5-8|12
@@ -90,18 +99,20 @@ public class MetadataTools {
      * sheet.name = !说明
      * ## 页签名称填充至哪个字段
      * sheet.name.fill.to = dept
-     * 
+     * ## 行序号填充至哪个字段
+     * row.index.fill.to = index
+     *
      * ## 字段转换规则(支持多个转换规则)
      * rules.rate = { clear:"[^\\.\\d]" }, { number:"int" }, { ignoreIllegalValue:true }, { rate:100 }
      * rules.tags = { split:"|" }
      * rules.positive = { map:{ true:"已转正|是|Y", false:"未转正|否|N" } }
      * rules.gender = { map:{ UNKNOWN:"未知|0", MALE:"男|1", FEMALE:"女|2" } }
      * rules.birthday = { date:"yyyy/MM/dd" }
-     * 
+     *
      * ## 将多个字段复制合并到一个字段
      * copy.concat = { keywords:"userName,nickName,deptName" }
      * </pre>
-     * 
+     *
      * @param properties 配置内容
      * @return XMetadata
      */
@@ -241,6 +252,8 @@ public class MetadataTools {
 
         // Sheet名称填充至哪个字段
         String sheetNameFillTo = PropertyTools.getString(properties, "sheet.name.fill.to", false);
+        // 行序号填充至哪个字段
+        String rowIndexFillTo = PropertyTools.getString(properties, "row.index.fill.to", false);
 
         // 解析字段复制合并参数
         String sCopyConcat = PropertyTools.getString(properties, "copy.concat", false);
@@ -253,6 +266,7 @@ public class MetadataTools {
         metadata.setHeaderRows(headerRows); // 表头所在的行号
         metadata.setFooterRows(footerRows); // 页脚所在的行号
         metadata.setSheetNameFillTo(sheetNameFillTo); // Sheet名称填充至哪个字段
+        metadata.setRowIndexFillTo(rowIndexFillTo); // 行序号填充至哪个字段
         metadata.setSheetIndexs(sheetIndexs); // Sheet序号配置
         metadata.setSheetNames(sheetNames); // Sheet名称配置
         if (!skipRowWhen.isEmpty()) {
@@ -269,7 +283,7 @@ public class MetadataTools {
 
     /**
      * 解析单元格规则
-     * 
+     *
      * @param jsonString JSON字符串
      * @return 单元格规则
      */
@@ -285,20 +299,16 @@ public class MetadataTools {
             jsonString = "[" + jsonString + "]";
         }
         // 转换为JSON数组
-        JSONArray array;
+        List<Map<String, Object>> array;
         try {
-            array = JSON.parseArray(jsonString);
+            array = JsonTools.parseAsMaps(jsonString);
         } catch (Exception e) {
             log.warn("CellRuleError, json string format error: " + jsonString, e);
             return null;
         }
         // 逐一解析
         List<CellRule> rules = new ArrayList<>();
-        for (Object i : array) {
-            if (!(i instanceof JSONObject)) {
-                continue;
-            }
-            JSONObject json = (JSONObject) i;
+        for (Map<String, Object> json : array) {
             for (Entry<String, Object> entry : json.entrySet()) {
                 String key = entry.getKey();
                 if (VerifyTools.isBlank(key)) {
@@ -320,7 +330,7 @@ public class MetadataTools {
 
     /**
      * 解析单元格值的判断条件
-     * 
+     *
      * @param jsonString JSON字符串: { A:"NULL" }, { B:"小计", H:"元" }, { B:"总计", H:"元" }
      * @return 条件对象
      */
@@ -332,20 +342,16 @@ public class MetadataTools {
             jsonString = "[" + jsonString + "]";
         }
         // 转换为JSON数组
-        JSONArray array;
+        List<Map<String, Object>> array;
         try {
-            array = JSON.parseArray(jsonString);
+            array = JsonTools.parseAsMaps(jsonString);
         } catch (Exception e) {
-            log.warn("ContainsTextConditionError, json string format error: " + jsonString, e);
+            log.warn("ContainsTextConditionError, json array string format error: " + jsonString, e);
             return null;
         }
         // 逐一解析
         List<List<Item>> conditions = new ArrayList<>();
-        for (Object i : array) {
-            if (!(i instanceof JSONObject)) {
-                continue;
-            }
-            JSONObject json = (JSONObject) i;
+        for (Map<String, Object> json : array) {
             if (json.isEmpty()) {
                 continue;
             }
@@ -380,20 +386,16 @@ public class MetadataTools {
             jsonString = "[" + jsonString + "]";
         }
         // 转换为JSON数组
-        JSONArray array;
+        List<Map<String, Object>> array;
         try {
-            array = JSON.parseArray(jsonString);
+            array = JsonTools.parseAsMaps(jsonString);
         } catch (Exception e) {
             log.warn("CopyConcatFieldsError, json string format error: " + jsonString, e);
             return null;
         }
         // 逐一解析
         List<CopyConcat> copyConcatFields = new ArrayList<>();
-        for (Object i : array) {
-            if (!(i instanceof JSONObject)) {
-                continue;
-            }
-            JSONObject json = (JSONObject) i;
+        for (Map<String, Object> json : array) {
             if (json.isEmpty()) {
                 continue;
             }
@@ -430,7 +432,7 @@ public class MetadataTools {
     /**
      * 解析字段列表配置<br>
      * 星号开头或(*)结尾的字段为必填字段: [*name] or [name(*)]
-     * 
+     *
      * @param text 配置内容
      * @return 字段配置对象
      */
@@ -455,7 +457,7 @@ public class MetadataTools {
     /**
      * 解析字段列表配置<br>
      * 星号开头或(*)结尾的字段为必填字段: [*name] or [name(*)]
-     * 
+     *
      * @param sheet Sheet
      * @param fieldRows 字段数据所在的行
      * @return 字段配置对象
@@ -506,7 +508,7 @@ public class MetadataTools {
     /**
      * 解析表头数据<br>
      * 星号开头或(*)结尾的字段为必填字段: [* 姓名] or [姓名 (*)]
-     * 
+     *
      * @param sheet Sheet
      * @param headerRows 表头数据所在的行
      * @param fieldInfos 字段数据
@@ -564,17 +566,17 @@ public class MetadataTools {
      * 从excel中读取转换规则<br>
      * 4列, 顺序为: 名称|KEY|类型|规则, 例如:<br>
      * <pre>
-        名称    KEY     类型    规则
-        整数    int     number  int
-        长整数  long    number  long
-        浮点数  double  number  double
-        布尔值  boolean map     true:是|Y|1, false:否|N|0
-        日期    date    date    yyyy-MM-dd
-        时间    time    date    HH:mm:ss
-        时分    hhmm    date    HH:mm
-        性别    gender  map     0:未知, 1:男, 2:女
+     * 名称    KEY     类型    规则
+     * 整数    int     number  int
+     * 长整数  long    number  long
+     * 浮点数  double  number  double
+     * 布尔值  boolean map     true:是|Y|1, false:否|N|0
+     * 日期    date    date    yyyy-MM-dd
+     * 时间    time    date    HH:mm:ss
+     * 时分    hhmm    date    HH:mm
+     * 性别    gender  map     0:未知, 1:男, 2:女
      * </pre>
-     * 
+     *
      * @param wb Excel文件对象
      * @param sheetName Sheet名称
      * @return 转换规则
@@ -586,7 +588,7 @@ public class MetadataTools {
 
     /**
      * 从excel中读取转换规则
-     * 
+     *
      * @param wb Excel文件对象
      * @param sheetName Sheet名称
      * @param columnFields 列字段顺序, 其中key|type|options三列必不可少
@@ -609,11 +611,11 @@ public class MetadataTools {
 
     private static XMetadata newRuleMetadata(String sheetName, String columnFields, int skipRows) {
         VerifyTools.requireNotBlank(sheetName, "sheetName");
-        Properties config = new Properties();
-        config.put("field.names", columnFields);
-        config.put("skip.rows", skipRows);
-        config.put("sheet.name", sheetName);
-        return parseProperties(config);
+        XMetadata instance = new XMetadata();
+        instance.setSkipRows(skipRows);
+        instance.setFieldInfos(parseFieldInfoByText(columnFields));
+        instance.setSheetNames(new NameListCondition(sheetName));
+        return instance;
     }
 
     private static class CellRuleCallback extends ImportCallback {
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/files/ImageTools.java b/tools/src/main/java/com/gitee/qdbp/tools/files/ImageTools.java
index 43faf9a6c46b70a46daa0bd5718bfdc500855f45..f7c61988aabf6744a4bb661d0297e88420c66638 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/files/ImageTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/files/ImageTools.java
@@ -1,7 +1,7 @@
 package com.gitee.qdbp.tools.files;
 
 import java.awt.Color;
-import java.awt.Graphics;
+import java.awt.Graphics2D;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -10,6 +10,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Hashtable;
 import java.util.Iterator;
+import java.util.List;
 import javax.imageio.ImageIO;
 import javax.imageio.ImageReader;
 import javax.imageio.stream.ImageInputStream;
@@ -32,7 +33,7 @@ public class ImageTools {
 
     /**
      * 给图片增加边距
-     * 
+     *
      * @param image 图片
      * @param top 上边距
      * @param bottom 下边距
@@ -46,37 +47,37 @@ public class ImageTools {
 
     /**
      * 给图片增加边距
-     * 
+     *
      * @param image 图片
-     * @param color 填充颜色
+     * @param bgColor 填充边距的背景色
      * @param top 上边距
      * @param bottom 下边距
      * @param left 左边距
      * @param right 右边距
      * @return 新图片
      */
-    public static BufferedImage paddingImage(BufferedImage image, Color color, int top, int bottom, int left,
-            int right) {
+    public static BufferedImage paddingImage(BufferedImage image, Color bgColor,
+            int top, int bottom, int left, int right) {
         int w = image.getWidth();
         int h = image.getHeight();
         int width = w + left + right;
         int height = h + top + bottom;
         BufferedImage background = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
         // 背景默认是黑色的, 填充为指定颜色
-        if (color != null) {
-            Graphics g = background.getGraphics();
-            g.setColor(color);
+        if (bgColor != null) {
+            Graphics2D g = (Graphics2D) background.getGraphics();
+            g.setBackground(bgColor);
             if (top > 0) {
-                g.fillRect(0, 0, width, top);
+                g.clearRect(0, 0, width, top);
             }
             if (bottom > 0) {
-                g.fillRect(0, height - bottom, width, bottom);
+                g.clearRect(0, height - bottom, width, bottom);
             }
             if (left > 0) {
-                g.fillRect(0, 0, left, height);
+                g.clearRect(0, 0, left, height);
             }
             if (right > 0) {
-                g.fillRect(0, width - right, right, height);
+                g.clearRect(width - right, 0, right, height);
             }
         }
         // 复制图片
@@ -84,6 +85,196 @@ public class ImageTools {
         return background;
     }
 
+    /**
+     * 纵向合并图片
+     *
+     * @param imageList 图片列表
+     */
+    public static BufferedImage mergeImage(List<BufferedImage> imageList) {
+        return mergeImage(imageList, Color.WHITE, 0, 0, 0, 0);
+    }
+
+    /**
+     * 纵向合并图片
+     *
+     * @param imageList 图片列表
+     * @param top 上边距
+     * @param bottom 下边距
+     * @param left 左边距
+     * @param right 右边距
+     */
+    public static BufferedImage mergeImage(List<BufferedImage> imageList, int top, int bottom, int left, int right) {
+        return mergeImage(imageList, Color.WHITE, top, bottom, left, right);
+    }
+
+    /**
+     * 纵向合并图片
+     *
+     * @param imageList 图片列表
+     * @param bgColor 填充边距的背景色
+     * @param top 上边距
+     * @param bottom 下边距
+     * @param left 左边距
+     * @param right 右边距
+     */
+    public static BufferedImage mergeImage(List<BufferedImage> imageList, Color bgColor,
+            int top, int bottom, int left, int right) {
+        int maxHeight = top + bottom;
+        int maxWidth = left + right;
+        for (BufferedImage image : imageList) {
+            maxHeight += image.getHeight();
+            if (maxWidth < image.getWidth()) {
+                maxWidth = image.getWidth();
+            }
+        }
+        BufferedImage merged = new BufferedImage(maxWidth, maxHeight, BufferedImage.TYPE_3BYTE_BGR);
+        if (bgColor != null) {
+            Graphics2D g = (Graphics2D) merged.getGraphics();
+            g.setBackground(bgColor);
+            if (top > 0) {
+                g.clearRect(0, 0, maxWidth, top);
+            }
+            if (bottom > 0) {
+                g.clearRect(0, maxHeight - bottom, maxWidth, bottom);
+            }
+            if (left > 0) {
+                g.clearRect(0, 0, left, maxHeight);
+            }
+            if (right > 0) {
+                g.clearRect(maxWidth - right, 0, right, maxHeight);
+            }
+        }
+        for (int i = 0; i < imageList.size(); i++) {
+            int usedHeight = top;
+            if (i > 0) {
+                int currentImageIndex = i;
+                while ((currentImageIndex - 1) >= 0) {
+                    usedHeight += imageList.get(currentImageIndex - 1).getHeight();
+                    currentImageIndex--;
+                }
+            }
+            BufferedImage image = imageList.get(i);
+            for (int h = 0; h < image.getHeight(); h++) {
+                for (int w = 0; w < image.getWidth(); w++) {
+                    merged.setRGB(w + left, h + usedHeight, image.getRGB(w, h));
+                }
+                Color bg = bgColor == null ? Color.WHITE : bgColor;
+                for (int w = image.getWidth(); w < maxWidth; w++) {
+                    merged.setRGB(w + left, h + usedHeight, bg.getRGB());
+                }
+            }
+        }
+        return merged;
+    }
+
+    /**
+     * 删除多余的白边
+     *
+     * @param image 原图片
+     * @return 修改后的图片
+     */
+    public static BufferedImage removeImagePadding(BufferedImage image) {
+        int width = image.getWidth();
+        int height = image.getHeight();
+        int maxX = width - 1;
+        int maxY = height - 1;
+
+        int rgb1 = image.getRGB(0, 0); // 左上角
+        int rgb2 = image.getRGB(maxX, 0); // 右上角
+        int rgb3 = image.getRGB(0, maxY); // 左下角
+        int rgb4 = image.getRGB(maxX, maxY); // 右下角
+
+        int leftX = getLeftPadding(image, rgb1);
+        if (leftX < 0 && rgb1 != rgb3) {
+            // 如果左上角的颜色不满足要求, 则取左下角的颜色重试
+            leftX = getLeftPadding(image, rgb3);
+        }
+        int topY = getTopPadding(image, rgb1);
+        if (topY < 0 && rgb1 != rgb2) {
+            // 如果左上角的颜色不满足要求, 则取左上角的颜色重试
+            topY = getLeftPadding(image, rgb2);
+        }
+        int rightX = getRightPadding(image, rgb4);
+        if (rightX < 0 && rgb4 != rgb2) {
+            // 如果右下角的颜色不满足要求, 则取左上角的颜色重试
+            rightX = getLeftPadding(image, rgb2);
+        }
+        int bottomY = getBottomPadding(image, rgb4);
+        if (bottomY < 0 && rgb4 != rgb3) {
+            // 如果右下角的颜色不满足要求, 则取左下角的颜色重试
+            bottomY = getLeftPadding(image, rgb3);
+        }
+
+        // 如果topY,leftX返回-1,说明上边或左边没有白边,此时坐标取0
+        int x1 = Math.max(0, leftX);
+        int y1 = Math.max(0, topY);
+
+        // bottomY,rightX返回-1,说明下边或右边没有白边,此时坐标分别取图片的高度和宽度
+        // 宽
+        int x2 = rightX < 0 ? maxX : rightX;
+        // 高
+        int y2 = bottomY < 0 ? maxY : bottomY;
+
+        // 如果四个边都没有白边, 就返回null
+        if (x1 == 0 && y1 == 0 && x2 == width && y2 == height) {
+            return image;
+        }
+
+        return image.getSubimage(x1, y1, x2 - x1, y2 - y1);
+    }
+
+    // 从上往下扫描,取上边最突出的点的Y坐标
+    private static int getTopPadding(BufferedImage originImage, int baseRgb) {
+        for (int y = 0; y < originImage.getHeight(); y++) {
+            for (int x = 0; x < originImage.getWidth(); x++) {
+                int rgb = originImage.getRGB(x, y);
+                if (rgb != baseRgb) {
+                    return y - 1;
+                }
+            }
+        }
+        return -1;
+    }
+
+    // 从左往右扫描,取左边最突出的点的X坐标
+    private static int getLeftPadding(BufferedImage originImage, int baseRgb) {
+        for (int x = 0; x < originImage.getWidth(); x++) {
+            for (int y = 0; y < originImage.getHeight(); y++) {
+                int rgb = originImage.getRGB(x, y);
+                if (rgb != baseRgb) {
+                    return x - 1;
+                }
+            }
+        }
+        return -1;
+    }
+
+    // 从下往上扫描,取下边最突出的点的Y坐标
+    private static int getBottomPadding(BufferedImage originImage, int baseRgb) {
+        for (int y = originImage.getHeight() - 1; y >= 0; y--) {
+            for (int x = originImage.getWidth() - 1; x >= 0; x--) {
+                int rgb = originImage.getRGB(x, y);
+                if (rgb != baseRgb) {
+                    return y + 1;
+                }
+            }
+        }
+        return -1;
+    }
+
+    // 从右往左扫描,取右边最突出的X坐标
+    private static int getRightPadding(BufferedImage originImage, int baseRgb) {
+        for (int x = originImage.getWidth() - 1; x >= 0; x--) {
+            for (int y = originImage.getHeight() - 1; y >= 0; y--) {
+                int rgb = originImage.getRGB(x, y);
+                if (rgb != baseRgb) {
+                    return x + 1;
+                }
+            }
+        }
+        return -1;
+    }
+
     /**
      * 生成缩略图<br>
      * 截取图片的中间部分(不产生变形)<br>
@@ -162,7 +353,7 @@ public class ImageTools {
 
     /**
      * 生成二维码
-     * 
+     *
      * @param content 内容
      * @param size 尺寸
      * @param os 输出流
@@ -173,7 +364,7 @@ public class ImageTools {
 
     /**
      * 生成二维码
-     * 
+     *
      * @param content 内容
      * @param size 尺寸
      * @param margin 边距
@@ -203,7 +394,7 @@ public class ImageTools {
 
     /**
      * 重设白边框宽度
-     * 
+     *
      * @param matrix BitMatrix
      * @param margin 边框宽度
      * @return 新的BitMatrix
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
index 7b3ffb554ca4795cdd9c111100214fe126a0561e..c2a96ae5937cd86263d9dccc459ccf653590f87d 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/BaseHttpHandler.java
@@ -2,20 +2,43 @@ package com.gitee.qdbp.tools.http;
 
 import java.util.HashMap;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.gitee.qdbp.able.result.ResponseMessage;
-import com.gitee.qdbp.able.result.ResultCode;
-import com.gitee.qdbp.tools.utils.VerifyTools;
 import org.apache.http.Header;
 import org.apache.http.HeaderIterator;
+import org.apache.http.client.CookieStore;
+import org.apache.http.impl.client.BasicCookieStore;
 import org.apache.http.message.BasicHeader;
 import org.apache.http.message.HeaderGroup;
+import com.gitee.qdbp.able.result.ResponseMessage;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 public class BaseHttpHandler implements IHttpHandler {
 
+    private CookieStore cookieStore;
     private HeaderGroup headers;
 
+    /** 获取Cookie存储器 **/
+    public CookieStore getCookieStore() {
+        return cookieStore;
+    }
+
+    /** 设置Cookie存储器 **/
+    protected void setCookieStore(CookieStore cookieStore) {
+        this.cookieStore = cookieStore;
+    }
+
+    /** 设置是否保持Cookie **/
+    protected void setKeepCookie(boolean keep) {
+        if (keep) {
+            this.cookieStore = new BasicCookieStore();
+        } else {
+            this.cookieStore = null;
+        }
+    }
+
     /** 追加header参数 **/
     protected void addHeader(String name, String value) {
         if (VerifyTools.isAnyBlank(name, value)) {
@@ -27,6 +50,13 @@ public class BaseHttpHandler implements IHttpHandler {
         this.headers.addHeader(new BasicHeader(name, value));
     }
 
+    /** 追加header参数 **/
+    protected void addHeaders(Map<String, ?> headers) {
+        for (Map.Entry<String, ?> entry : headers.entrySet()) {
+            addHeader(entry.getKey(), ConvertTools.toString(entry.getValue()));
+        }
+    }
+
     /** 删除header参数 **/
     protected void removeHeader(String name) {
         if (VerifyTools.isBlank(name)) {
@@ -43,6 +73,26 @@ public class BaseHttpHandler implements IHttpHandler {
         }
     }
 
+    /** 删除header参数 **/
+    protected void removeHeaders(String... names) {
+        if (VerifyTools.isBlank(names)) {
+            return;
+        }
+        if (this.headers == null) {
+            return;
+        }
+        Map<String, ?> maps = new HashMap<>();
+        for (String name : names) {
+            maps.put(name.toLowerCase(), null);
+        }
+        for (HeaderIterator i = this.headers.iterator(); i.hasNext();) {
+            Header header = i.nextHeader();
+            if (maps.containsKey(header.getName().toLowerCase())) {
+                i.remove();
+            }
+        }
+    }
+
     /** 遍历header参数 **/
     protected HeaderIterator headerIterator() {
         if (this.headers == null) {
@@ -52,8 +102,9 @@ public class BaseHttpHandler implements IHttpHandler {
         }
     }
 
-    /** 获取全部header参数 **/
-    public Header[] getAllHeaders() {
+    /** 获取基础header参数 **/
+    @Override
+    public Header[] getBaseHeaders() {
         return this.headers == null ? null : this.headers.getAllHeaders();
     }
 
@@ -82,12 +133,12 @@ public class BaseHttpHandler implements IHttpHandler {
      * @throws Exception 解析异常
      */
     public ResponseMessage parseResult(HttpUrl hurl, String string) throws RemoteServiceException, Exception {
-        JSONObject json = JSON.parseObject(string);
+        Map<String, Object> json = JsonTools.parseAsMap(string);
         ResponseMessage result = new ResponseMessage();
-        result.setCode(json.getString("code"));
-        result.setMessage(json.getString("message"));
+        result.setCode(MapTools.getString(json, "code"));
+        result.setMessage(MapTools.getString(json, "message"));
         result.setBody(json.get("body"));
-        result.setExtra(json.getJSONObject("extra"));
+        result.setExtra(MapTools.getSubMap(json, "extra"));
 
         if (ResultCode.SUCCESS.name().equals(result.getCode())) {
             return result;
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java b/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
index a12fdb9733fc68a20505c066b3261aa13c4927cc..ed69864db2daeff3d58837a186c54c2b6c3b7fc8 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/HttpTools.java
@@ -9,13 +9,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.util.TypeUtils;
-import com.gitee.qdbp.able.beans.KeyString;
-import com.gitee.qdbp.able.exception.ServiceException;
-import com.gitee.qdbp.able.result.ResponseMessage;
-import com.gitee.qdbp.able.result.ResultCode;
-import com.gitee.qdbp.tools.utils.VerifyTools;
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpMessage;
@@ -33,6 +26,12 @@ import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.util.EntityUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResponseMessage;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
  * HTTP请求工具类<pre>
@@ -40,6 +39,14 @@ import org.slf4j.LoggerFactory;
     HttpTools.form.post("http://127.0.0.1/api/xxx", map);
     // 以json方式提交参数: {x:11,y:22,z:33}
     HttpTools.json.post("http://127.0.0.1/api/xxx", map);
+
+    // 设置Header示例
+    SimpleHttpHandler httpHandler = new SimpleHttpHandler();
+    httpHandler.addHeader("token", "xxx-token");
+    httpHandler.setKeepCookie(true);
+    HttpTools http = new HttpJsonImpl(httpHandler);
+    System.out.println(http.post("http://www.xxx.com/api/xxx"));
+
     // 自定义超时时间示例
     RequestConfig config = RequestConfig.custom() // 自定义配置
             .setConnectionRequestTimeout(1000) // 从连接池获取连接的超时时间
@@ -47,7 +54,6 @@ import org.slf4j.LoggerFactory;
             .setSocketTimeout(10 * 1000) // 客户端从服务器读取数据的超时时间
             .build();
     HttpTools http = new HttpFormImpl(config);
-    System.out.println(http.post("http://www.xxx.com/api/xxx"));
  * </pre>
  * 
  * @author zhaohuihua
@@ -141,7 +147,7 @@ public class HttpTools {
         } catch (RemoteServiceException e) {
             throw e;
         } catch (Exception e) {
-            throw new ResultParseException("Http request success, but JSON.parseObject error. " + hurl, e);
+            throw new ResultParseException("Http request success, but parseJsonObject error. " + hurl, e);
         }
 
         return result;
@@ -178,9 +184,9 @@ public class HttpTools {
         }
 
         try {
-            return TypeUtils.castToJavaBean(body, type);
+            return JsonTools.convert(body, type);
         } catch (Exception e) {
-            throw new ResultParseException("Http request success, but TypeUtils.castToJavaBean error. " + hurl, e);
+            throw new ResultParseException("Http request success, but convertToJavaBean error. " + hurl, e);
         }
     }
 
@@ -216,9 +222,9 @@ public class HttpTools {
 
         if (body instanceof String) {
             try {
-                return JSON.parseArray((String) body, type);
+                return JsonTools.parseAsObjects((String) body, type);
             } catch (Exception e) {
-                throw new ResultParseException("Http request success, but JSON.parseArray error. " + hurl, e);
+                throw new ResultParseException("Http request success, but parseJsonArray error. " + hurl, e);
             }
         }
 
@@ -240,7 +246,7 @@ public class HttpTools {
         List<T> results = new ArrayList<>();
         for (Object item : objects) {
             try {
-                results.add(TypeUtils.castToJavaBean(item, type));
+                results.add(JsonTools.convert(item, type));
             } catch (Exception e) {
                 throw new ResultParseException("Http request success, but TypeUtils.castToJavaBean error. " + hurl, e);
             }
@@ -313,6 +319,9 @@ public class HttpTools {
 
         // 创建HttpClientBuilder
         HttpClientBuilder builder = HttpClientBuilder.create();
+        if (httpHandler.getCookieStore() != null) {
+            builder.setDefaultCookieStore(httpHandler.getCookieStore());
+        }
         // HttpClient
         try (CloseableHttpClient client = builder.build()) {
             HttpGet get = new HttpGet(uri);
@@ -357,6 +366,9 @@ public class HttpTools {
     public <P> String post(String url, Map<String, P> params) throws HttpException {
         // 创建HttpClientBuilder
         HttpClientBuilder builder = HttpClientBuilder.create();
+        if (httpHandler.getCookieStore() != null) {
+            builder.setDefaultCookieStore(httpHandler.getCookieStore());
+        }
         // HttpClient
         try (CloseableHttpClient client = builder.build()) {
             HttpPost post = new HttpPost(url);
@@ -387,6 +399,9 @@ public class HttpTools {
     public <P> String upload(String url, Map<String, P> params) throws HttpException {
         // 创建HttpClientBuilder
         HttpClientBuilder builder = HttpClientBuilder.create();
+        if (httpHandler.getCookieStore() != null) {
+            builder.setDefaultCookieStore(httpHandler.getCookieStore());
+        }
         // HttpClient
         try (CloseableHttpClient client = builder.build()) {
             HttpPost post = new HttpPost(url);
@@ -514,9 +529,9 @@ public class HttpTools {
 
     /** 发送请求前设置header参数等操作 **/
     protected void onBeforeExecute(HttpMessage hm) {
-        Header[] allHeaders = httpHandler.getAllHeaders();
-        if (VerifyTools.isNotBlank(allHeaders)) {
-            hm.setHeaders(allHeaders);
+        Header[] baseHeaders = httpHandler.getBaseHeaders();
+        if (VerifyTools.isNotBlank(baseHeaders)) {
+            hm.setHeaders(baseHeaders);
         }
     }
 
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/IHttpHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/http/IHttpHandler.java
index b086b78a32aea4b463a89dbe50d235492b6a1690..3119ebfafe87d9464e463918b0e8fcdf53d0e7fd 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/IHttpHandler.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/IHttpHandler.java
@@ -3,6 +3,7 @@ package com.gitee.qdbp.tools.http;
 import java.util.Map;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import org.apache.http.Header;
+import org.apache.http.client.CookieStore;
 
 /**
  * 参数处理和结果解析
@@ -12,8 +13,11 @@ import org.apache.http.Header;
  */
 public interface IHttpHandler {
 
-    /** 获取全部header参数 **/
-    Header[] getAllHeaders();
+    /** 获取Cookie存储器 **/
+    CookieStore getCookieStore();
+
+    /** 获取基础header参数 **/
+    Header[] getBaseHeaders();
 
     /**
      * 填充基础参数, 如填充配置信息在的公共参数/计算摘要等操作
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForForm.java b/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForForm.java
index 6f3a24d6181b79587122425505aa4a61d6759a81..3f5b0168fc619a2c2ba09d26f36c7bfffe65ecc4 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForForm.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForForm.java
@@ -7,8 +7,8 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import com.alibaba.fastjson.JSON;
 import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.tools.utils.JsonTools;
 import org.apache.http.HttpEntity;
 import org.apache.http.NameValuePair;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
@@ -129,7 +129,7 @@ public class ParamSetterForForm implements IParamSetter {
             string = value.toString();
             builder.addTextBody(key, string, contentType);
         } else {
-            string = JSON.toJSONString(value);
+            string = JsonTools.toJsonString(value);
             builder.addTextBody(key, string, contentType);
         }
         if (logs != null) {
@@ -144,7 +144,7 @@ public class ParamSetterForForm implements IParamSetter {
         } else if (value instanceof CharSequence) {
             string = value.toString();
         } else {
-            string = JSON.toJSONString(value);
+            string = JsonTools.toJsonString(value);
         }
         if (logs != null) {
             logs.add(new KeyString(key, string));
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForJson.java b/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForJson.java
index e8f75750b17a37c3140e98b5798b47e9099b0ba3..69274b997fe664070ea3bce03d7c15243ae5151a 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForJson.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/ParamSetterForJson.java
@@ -2,12 +2,12 @@ package com.gitee.qdbp.tools.http;
 
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.gitee.qdbp.able.beans.KeyString;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.utils.URIBuilder;
 import org.apache.http.entity.ContentType;
 import org.apache.http.entity.StringEntity;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.tools.utils.JsonTools;
 
 /**
  * 以application/json的方式提交请求参数
@@ -29,7 +29,7 @@ public class ParamSetterForJson implements IParamSetter {
 
     @Override
     public <P> void setPostParams(HttpPost method, Map<String, P> params, List<KeyString> logs) {
-        String json = JSON.toJSONString(params);
+        String json = JsonTools.toJsonString(params);
         if (logs != null) {
             logs.add(new KeyString(null, json));
         }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/http/SimpleHttpHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/http/SimpleHttpHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..9786e3c59357fa3ae4c636604053f0328cb0712a
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/http/SimpleHttpHandler.java
@@ -0,0 +1,43 @@
+package com.gitee.qdbp.tools.http;
+
+import org.apache.http.HeaderIterator;
+import org.apache.http.client.CookieStore;
+
+public class SimpleHttpHandler extends BaseHttpHandler {
+
+    /** 设置Cookie存储器 **/
+    @Override
+    public void setCookieStore(CookieStore cookieStore) {
+        super.setCookieStore(cookieStore);
+    }
+
+    /** 设置是否保持Cookie **/
+    public void setKeepCookie(boolean keep) {
+        super.setKeepCookie(keep);
+    }
+
+    /** 追加header参数 **/
+    @Override
+    public void addHeader(String name, String value) {
+        super.addHeader(name, value);
+    }
+
+    /** 删除header参数 **/
+    @Override
+    public void removeHeader(String name) {
+        super.removeHeader(name);
+    }
+
+    /** 删除header参数 **/
+    @Override
+    public void removeHeaders(String... names) {
+        super.removeHeaders(names);
+    }
+
+    /** 遍历header参数 **/
+    @Override
+    public HeaderIterator headerIterator() {
+        return super.headerIterator();
+    }
+
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6cc7cc05fa8b42dee4bd2535ce784a767bb8f04
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpChannel.java
@@ -0,0 +1,809 @@
+package com.gitee.qdbp.tools.sftp;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Vector;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.enums.FileErrorCode;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.matches.FileMatcher;
+import com.gitee.qdbp.able.model.file.PathInfo;
+import com.gitee.qdbp.tools.files.FileTools;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.Session;
+import com.jcraft.jsch.SftpATTRS;
+import com.jcraft.jsch.SftpException;
+
+/**
+ * SFTP操作类<br>
+ * 注意: 以/开头的路径表示当前SFTP用户的根路径, 而不是系统的根路径
+ *
+ * @author zhaohuihua
+ * @version 20230311
+ * @since 5.5.10
+ */
+public class SftpChannel implements AutoCloseable {
+
+    private final Session session;
+    private final ChannelSftp channel;
+
+    public SftpChannel(Session session, ChannelSftp channel) {
+        this.session = session;
+        this.channel = channel;
+    }
+
+    @Override
+    public void close() {
+        if (channel != null) {
+            channel.disconnect();
+        }
+        if (session != null) {
+            session.disconnect();
+        }
+    }
+
+    /** 当前目录 **/
+    public String pwd() {
+        return doGetPwd(channel, this.home());
+    }
+
+    /** 返回SFTP根目录 (系统绝对路径) **/
+    public String home() {
+        return doGetHome(channel);
+    }
+
+    /** 返回SFTP根目录 (系统绝对路径) **/
+    private static String doGetHome(ChannelSftp channel) {
+        try {
+            return channel.getHome();
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails("channel.getHome()");
+        }
+    }
+
+    /** 当前目录 **/
+    private static String doGetPwd(ChannelSftp channel, String home) {
+        String pwd;
+        try {
+            pwd = channel.pwd();
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails("channel.pwd()");
+        }
+        if (pwd.equals(home)) {
+            return "/";
+        } else {
+            return PathTools.concat("/", StringTools.removePrefix(pwd, home), "/");
+        }
+    }
+
+    /**
+     * 文件下载
+     *
+     * @param remotePath 远程文件路径: 既可以是文件名(下载当前目录下的文件), 也可以是全路径+文件名(根据绝对路径下载)
+     * @return 返回文件流
+     * @throws ServiceException 下载失败
+     */
+    public InputStream downloadFile(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            // 拆分文件目录和文件名
+            if (rp.endWithSlash) {
+                throw new ServiceException(FileErrorCode.FILE_PATH_ERROR)
+                        .setDetails("remotePath=" + rp.original);
+            }
+
+            // 下载文件
+            try {
+                return channel.get(rp.effectivePath());
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_DOWNLOAD_ERROR, e)
+                        .setDetails("remotePath=" + rp.original);
+            }
+        }
+    }
+
+    /**
+     * 文件下载并保存到指定位置
+     *
+     * @param remotePath 远程文件路径: 既可以是文件名(下载当前目录下的文件), 也可以是全路径+文件名(根据绝对路径下载)
+     * @param clientPath 本地文件路径: 既可以是文件目录(以/结尾,保存为远程文件名), 也可以是全路径+文件名(保存到绝对路径)
+     * @return 实际保存路径
+     * @throws ServiceException 下载失败<br>
+     * FILE_DOWNLOAD_ERROR 文件下载失败<br>
+     * FILE_SAVE_ERROR 文件保存失败
+     */
+    public String downloadSave(String remotePath, String clientPath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            // 拆分远程路径为文件目录和文件名
+            if (rp.endWithSlash || rp.name == null) {
+                throw new ServiceException(FileErrorCode.FILE_PATH_ERROR).setDetails("remotePath=" + rp);
+            }
+            // 拆分本地路径为文件目录和文件名
+            ClientPath cp = ClientPath.of(clientPath);
+            if (cp.endWithSlash) {
+                cp = cp.enter(rp.name);
+            }
+
+            // 拼接路径
+            // 判断目标文件是否存在
+            SftpPath remoteFile = getFileInfo(rp);
+            if (remoteFile == null || !remoteFile.isFile()) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails("remotePath=" + rp);
+            }
+            // 如果本地目录不存在则创建
+            String savePath = cp.fullPathOnRoot();
+            FileTools.mkdirsIfNotExists(savePath);
+            try (OutputStream output = new FileOutputStream(savePath)) {
+                // 下载文件并保存
+                try {
+                    channel.get(rp.effectivePath(), output);
+                    return savePath;
+                } catch (SftpException e) {
+                    throw new ServiceException(FileErrorCode.FILE_DOWNLOAD_ERROR, e)
+                            .setDetails("remotePath=" + rp);
+                }
+            } catch (IOException e) {
+                throw new ServiceException(FileErrorCode.FILE_SAVE_ERROR, e).setDetails("clientPath=" + cp);
+            }
+        }
+    }
+
+    /**
+     * 文件上传
+     *
+     * @param input 输入流
+     * @param remotePath 远程文件路径: 既可以是文件名(上传到当前目录), 也可以是全路径+文件名(保存到绝对路径)
+     */
+    public void uploadFile(InputStream input, String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            if (rp.endWithSlash) {
+                throw new ServiceException(FileErrorCode.FILE_PATH_ERROR).setDetails("remotePath=" + rp);
+            }
+
+            // 创建上传目录
+            doMakeDirectory(rp.folder);
+
+            // 文件上传
+            try {
+                channel.put(input, rp.effectivePath());
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e).setDetails("remotePath=" + rp);
+            }
+        }
+    }
+
+    /**
+     * 文件上传 (保存到当前目录)
+     *
+     * @param clientPath 本地文件路径: 全路径+文件名
+     */
+    public void uploadFile(String clientPath) {
+        uploadFile(clientPath, null);
+    }
+
+    /**
+     * 文件上传
+     *
+     * @param clientPath 本地文件路径: 全路径+文件名
+     * @param remotePath 远程文件路径: 既可以是文件目录(以/结尾,保存为本地文件名), 也可以是全路径+文件名(保存到绝对路径)
+     */
+    public void uploadFile(String clientPath, String remotePath) {
+        // 拆分本地路径为文件目录和文件名
+        ClientPath cp = ClientPath.of(clientPath);
+        if (cp.endWithSlash || cp.name == null) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR).setDetails("clientPath=" + cp);
+        }
+        // 判断文件是否存在
+        File clientFile = new File(cp.fullPathOnRoot());
+        if (!clientFile.exists()) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND)
+                    .setDetails("clientPath=" + cp);
+        }
+        if (clientFile.isDirectory()) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR)
+                    .setDetails("clientPath=" + cp);
+        }
+        // 拆分远程路径为文件目录和文件名
+        try (RemotePath rp = parseRemotePath(remotePath, cp.name)) {
+            // 创建上传目录
+            doMakeDirectory(rp.folder);
+
+            // 文件上传
+            try (InputStream input = new FileInputStream(clientFile)) {
+                try {
+                    channel.put(input, rp.effectivePath());
+                } catch (SftpException e) {
+                    throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e).setDetails("remotePath=" + rp);
+                }
+            } catch (IOException e) {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails("clientPath=" + cp);
+            }
+        }
+    }
+
+    private RemotePath parseRemotePath(String remotePath, String clientFileName) {
+        RemotePath rp;
+        if (VerifyTools.isBlank(remotePath)) {
+            rp = RemotePath.of(channel, clientFileName);
+        } else {
+            rp = RemotePath.of(channel, remotePath);
+            if (rp.endWithSlash) {
+                rp = rp.enter(clientFileName);
+            }
+        }
+        return rp;
+    }
+
+    private boolean isFileNotFound(SftpException e) {
+        String message = e.getMessage().toLowerCase();
+        return message.contains("no such") || message.contains("not found") || message.contains("not find");
+    }
+
+    public PathInfo info(String remotePath) {
+        return getFileInfo(RemotePath.of(channel, remotePath));
+    }
+
+    private SftpPath getFileInfo(RemotePath rp) {
+        try {
+            SftpATTRS attrs = channel.lstat(rp.effectivePath());
+            return newSftpFile(rp, attrs);
+        } catch (SftpException e) {
+            if (isFileNotFound(e)) {
+                return null;
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_HANDLE_ERROR, e)
+                        .setDetails("remotePath=" + rp);
+            }
+        }
+    }
+
+    /**
+     * 判断指定的文件或目录是否存在
+     *
+     * @param remotePath 远程文件路径: 可以是相对路径文件名/相对路径目录/绝对路径文件名/绝对路径目录
+     * @return 是否存在
+     */
+    public boolean exists(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            try {
+                SftpATTRS attrs = channel.lstat(rp.effectivePath());
+                return attrs != null;
+            } catch (SftpException e) {
+                if (isFileNotFound(e)) {
+                    return false;
+                } else {
+                    throw new ServiceException(FileErrorCode.FILE_HANDLE_ERROR, e).setDetails("remotePath=" + rp);
+                }
+            }
+        }
+    }
+
+    /**
+     * 判断指定路径是不是文件
+     *
+     * @param remotePath 远程文件路径: 可以是相对路径文件名/相对路径目录/绝对路径文件名/绝对路径目录
+     * @return 是否存在
+     */
+    public boolean isFile(String remotePath) {
+        return !isDirectory(remotePath);
+    }
+
+    /**
+     * 判断指定路径是不是目录
+     *
+     * @param remotePath 远程文件路径: 可以是相对路径文件名/相对路径目录/绝对路径文件名/绝对路径目录
+     * @return 是否存在
+     */
+    public boolean isDirectory(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            try {
+                SftpATTRS attrs = channel.lstat(rp.effectivePath());
+                return attrs.isDir() || attrs.isLink();
+            } catch (SftpException e) {
+                if (isFileNotFound(e)) {
+                    return false;
+                } else {
+                    throw new ServiceException(FileErrorCode.FILE_HANDLE_ERROR, e).setDetails("remotePath=" + rp);
+                }
+            }
+        }
+    }
+
+    public void mkdir(String remotePath) {
+        makeDirectory(remotePath);
+    }
+
+    public void makeDirectory(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            doMakeDirectory(rp.effectivePath());
+        }
+    }
+
+    private void doMakeDirectory(String remoteFolder) {
+        if (VerifyTools.isBlank(remoteFolder)) {
+            return;
+        }
+        List<String> names = StringTools.splits(remoteFolder, '/');
+        for (int i = 0; i < names.size(); i++) {
+            String name = names.get(i);
+            if (VerifyTools.isBlank(name)) {
+                continue;
+            }
+            String path = ConvertTools.joinToString(names.subList(0, i + 1), '/');
+            // 判断是否存在
+            SftpATTRS attrs = null;
+            try {
+                attrs = channel.lstat(path);
+            } catch (SftpException ignore) {
+            }
+            if (attrs != null) {
+                if (attrs.isDir() || attrs.isLink()) {
+                    continue;
+                } else {
+                    throw new ServiceException(FileErrorCode.FILE_PATH_ERROR).setDetails("remoteFolder=" + path);
+                }
+            }
+            // 创建目录
+            try {
+                channel.mkdir(path);
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_HANDLE_ERROR, e).setDetails("remoteFolder=" + path);
+            }
+        }
+    }
+
+    public void cd(String remoteFolder) {
+        doChangeDirectory(channel, remoteFolder);
+    }
+
+    public void toHome() {
+        String remoteFolder = this.home();
+        try {
+            channel.cd(remoteFolder);
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR, e)
+                    .setDetails("remoteFolder=" + remoteFolder);
+        }
+    }
+
+    private static void doChangeDirectory(ChannelSftp channel, String remoteFolder) {
+        if (VerifyTools.isBlank(remoteFolder)) {
+            return;
+        }
+        // 如果是绝对路径, 先跳转至home目录, 再按相对路径处理
+        String relativeFolder = remoteFolder;
+        if (PathTools.isAbsolutePath(remoteFolder)) {
+            doChangeToHome(channel);
+            relativeFolder = StringTools.trimLeft(remoteFolder, '/');
+        }
+        try {
+            channel.cd(relativeFolder);
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR, e).setDetails("remoteFolder=" + remoteFolder);
+        }
+    }
+
+    private static void doChangeToHome(ChannelSftp channel) {
+        String home = doGetHome(channel);
+        try {
+            channel.cd(home);
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_PATH_ERROR, e).setDetails("remoteFolder=/");
+        }
+    }
+
+    /**
+     * 列出指定目录下所有文件
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @return 文件列表
+     */
+    public List<PathInfo> listFiles(String remotePath) {
+        return listFiles(remotePath, null);
+    }
+
+    /**
+     * 列出指定目录下所有文件
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param matcher 过滤条件
+     * @return 文件列表
+     */
+    public List<PathInfo> listFiles(String remotePath, FileMatcher matcher) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            List<PathInfo> paths = doScanPaths(rp, 1, false);
+            return matcher == null ? paths : filterPaths(paths, matcher);
+        }
+    }
+
+    /**
+     * 递归扫描指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @return 文件列表
+     */
+    public List<PathInfo> scanFiles(String remotePath) {
+        return scanFiles(remotePath, null);
+    }
+
+    /**
+     * 递归扫描指定目录下所有文件
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param matcher 过滤条件
+     * @return 文件列表
+     */
+    public List<PathInfo> scanFiles(String remotePath, FileMatcher matcher) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            List<PathInfo> paths = doScanPaths(rp, 1, true);
+            return matcher == null ? paths : filterPaths(paths, matcher);
+        }
+    }
+
+    /**
+     * 列出指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @return 文件列表
+     */
+    public List<PathInfo> listPaths(String remotePath) {
+        return listPaths(remotePath, null);
+    }
+
+    /**
+     * 列出指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param matcher 过滤条件
+     * @return 文件列表
+     */
+    public List<PathInfo> listPaths(String remotePath, FileMatcher matcher) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            List<PathInfo> paths = doScanPaths(rp, 0, false);
+            return matcher == null ? paths : filterPaths(paths, matcher);
+        }
+    }
+
+    /**
+     * 递归扫描指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @return 文件列表
+     */
+    public List<PathInfo> scanPaths(String remotePath) {
+        return scanPaths(remotePath, null);
+    }
+
+    /**
+     * 递归扫描指定目录下所有文件或目录
+     *
+     * @param remotePath 远程文件路径: 既可以是目录名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param matcher 过滤条件
+     * @return 文件列表
+     */
+    public List<PathInfo> scanPaths(String remotePath, FileMatcher matcher) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            List<PathInfo> paths = doScanPaths(rp, 0, true);
+            return matcher == null ? paths : filterPaths(paths, matcher);
+        }
+    }
+
+    private List<PathInfo> filterPaths(List<PathInfo> paths, FileMatcher matcher) {
+        List<PathInfo> result = new ArrayList<>();
+        for (PathInfo path : paths) {
+            if (path instanceof SftpPath) {
+                SftpPath file = (SftpPath) path;
+                if (matcher.matches(file)) {
+                    result.add(file);
+                }
+            }
+        }
+        return result;
+    }
+
+    // fileType: 0=全部|1=文件|2=目录
+    private List<PathInfo> doScanPaths(RemotePath rp, int fileType, boolean recursive) {
+        Vector<?> vector;
+        try {
+            vector = channel.ls(VerifyTools.nvl(rp.effectivePath(), "."));
+        } catch (SftpException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e).setDetails("remotePath=" + rp);
+        }
+        List<PathInfo> result = new ArrayList<>();
+        for (Object item : vector) {
+            ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) item;
+            String fileName = entry.getFilename();
+            if (".".equals(fileName) || "..".equals(fileName)) {
+                continue;
+            }
+            // 判断是文件还是目录
+            boolean isDirectory = isDirectory(entry.getAttrs());
+            // 判断文件类型是否匹配
+            boolean matches;
+            if (isDirectory) {
+                matches = fileType == 0 || fileType == 2;
+            } else {
+                matches = fileType == 0 || fileType == 1;
+            }
+            // 下级对象
+            RemotePath next = rp.enter(fileName + (isDirectory ? "/" : ""));
+            // 文件类型匹配则加入结果集
+            if (matches) {
+                SftpPath file = newSftpFile(next, entry.getAttrs());
+                result.add(file);
+            }
+            // 递归且是目录则继续处理下级对象
+            if (recursive && isDirectory) {
+                result.addAll(doScanPaths(next, fileType, true));
+            }
+        }
+        return result;
+    }
+
+    private SftpPath newSftpFile(BasePath path, SftpATTRS attrs) {
+        String directory = path.invisiblePath();
+        String relativePath = path.effectivePath();
+
+        SftpPath file = new SftpPath(directory, relativePath);
+        if (attrs != null) {
+            boolean isDirectory = isDirectory(attrs);
+            file.setIsFile(!isDirectory);
+            file.setIsDirectory(isDirectory);
+            file.setLength(attrs.getSize());
+            file.setPermissions(attrs.getPermissions());
+            file.setUpdateTime(new Date(attrs.getMTime() * 1000L));
+        }
+        return file;
+    }
+
+    private boolean isDirectory(SftpATTRS attrs) {
+        if (attrs == null) {
+            return false;
+        } else {
+            return attrs.isDir() || attrs.isLink();
+        }
+    }
+
+    /**
+     * 文件重命名
+     *
+     * @param remotePath 远程文件路径: 既可以是文件名(当前目录), 也可以是全路径+文件名(绝对路径)
+     * @param newName 新文件名
+     */
+    public void rename(String remotePath, String newName) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            try {
+                channel.rename(rp.effectivePath(), newName);
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_RENAME_ERROR, e)
+                        .setDetails("remotePath={}, newName={}", rp, newName);
+            }
+        }
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param remotePath 远程文件路径: 既可以是文件名(当前目录), 也可以是全路径+文件名(绝对路径)
+     */
+    public boolean deleteFile(String remotePath) {
+        try (RemotePath rp = RemotePath.of(channel, remotePath)) {
+            // 判断目标文件是否存在
+            SftpPath remoteFile = getFileInfo(rp);
+            if (remoteFile == null || !remoteFile.isFile()) {
+                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND).setDetails("remotePath=" + rp.original);
+            }
+
+            try {
+                channel.rm(rp.effectivePath());
+                return true;
+            } catch (SftpException e) {
+                throw new ServiceException(FileErrorCode.FILE_DELETE_ERROR, e).setDetails("remotePath=" + rp);
+            }
+        }
+    }
+
+    /**
+     * 规范远程路径, 将远程路径统一切分为/home/pwd/folder/name共4个部分<br>
+     * home = SFTP的home目录<br>
+     * pwd = 当前所在路径; 如果传入的是以/开头的绝对路径, 此部分为空<br>
+     * folder = 入参的目录部分; 如果传入的是文件名, 此部分为空<br>
+     * name = 入参的文件名部分; 如果传入的是以/结尾的目录, 此部分也取目录的最后一段, 但isDirectory=true<br>
+     */
+    protected static class BasePath {
+        protected final String original;
+        protected final String home;
+        protected final String pwd;
+        protected final String folder;
+        protected final String name;
+        /** 入参是不是以/结尾 (如果明确以/结尾就是目录, 不是/结尾有可能是文件也有可能是目录) **/
+        protected final boolean endWithSlash;
+
+        protected BasePath(String original, String home, String pwd, String folder, String name,
+                boolean endWithSlash) {
+            this.original = original;
+            this.home = VerifyTools.isBlank(home) ? null : home;
+            this.pwd = trimPath(pwd);
+            this.folder = trimPath(folder);
+            this.name = trimPath(name);
+            this.endWithSlash = endWithSlash;
+        }
+
+        /** 相对于根目录的地址, 即全路径, 返回 /home/pwd/folder/name **/
+        public String fullPathOnRoot() {
+            return PathTools.concat(home, pwd, folder, name, endWithSlash ? "/" : "");
+        }
+
+        /** 相对于Home目录的地址, 返回 pwd/folder/name **/
+        public String fullPathOnHome() {
+            return PathTools.concat(pwd, folder, name, endWithSlash ? "/" : "");
+        }
+
+        /** 相对于根目录的地址, 即全路径, 返回 /home/pwd/folder/ **/
+        public String folderOnRoot() {
+            return PathTools.concat(home, pwd, folder, "/");
+        }
+
+        /** 相对于Home目录的地址, 返回 /pwd/folder/ **/
+        public String folderOnHome() {
+            return PathTools.concat(pwd, folder, "/");
+        }
+
+        /** 隐含目录, 不是参数传入的前缀路径 **/
+        public String invisiblePath() {
+            return PathTools.concat(home, pwd, "/");
+        }
+
+        /** 有效路径, 参数传入的路径 **/
+        public String effectivePath() {
+            return PathTools.concat(folder, name, endWithSlash ? "/" : "");
+        }
+
+        private static String trimPath(String path) {
+            if (VerifyTools.isBlank(path)) {
+                return null;
+            } else {
+                return StringTools.trimLeft(path, '/');
+            }
+        }
+
+        protected static KeyString splitFolderAndName(String path) {
+            if (VerifyTools.isBlank(path)) {
+                return new KeyString();
+            }
+            String tempPath = StringTools.trimRight(path, '/');
+            String folder;
+            String name;
+            if (tempPath.indexOf('/') < 0) {
+                folder = null;
+                name = tempPath;
+            } else {
+                folder = PathTools.removeFileName(tempPath);
+                name = PathTools.getFileName(tempPath);
+            }
+            return new KeyString(folder, name);
+        }
+    }
+
+    protected static class ClientPath extends BasePath {
+
+        protected ClientPath(String original, String home, String pwd, String folder, String name,
+                boolean endWithSlash) {
+            super(original, home, pwd, folder, name, endWithSlash);
+        }
+
+        /** 进入下级路径 **/
+        public ClientPath enter(String path) {
+            boolean endWithSlash = path.charAt(path.length() - 1) == '/';
+            String original = PathTools.concat(this.original, path);
+            KeyString temp = splitFolderAndName(path);
+            String folder = PathTools.concat(this.folder, this.name, temp.getKey());
+            String name = temp.getValue();
+            return new ClientPath(original, home, pwd, folder, name, endWithSlash);
+        }
+
+        @Override
+        public String toString() {
+            return fullPathOnRoot();
+        }
+
+        public static ClientPath of(String path) {
+            VerifyTools.requireNotBlank(path, "path");
+            String formatted = (".".equals(path) || "./".equals(path)) ? path : PathTools.formatPath(path);
+            boolean isAbsolute = formatted.length() > 0 && PathTools.isAbsolutePath(formatted);
+            boolean endWithSlash = formatted.length() > 0 && formatted.charAt(formatted.length() - 1) == '/';
+
+            String home;
+            if (formatted.startsWith("/")) {
+                String osName = System.getProperty("os.name");
+                if (osName != null && osName.startsWith("Windows")) {
+                    home = PathTools.formatPath(new File("/").getAbsolutePath());
+                } else {
+                    home = "/";
+                }
+                formatted = formatted.substring(1);
+            } else if (isAbsolute) {
+                // D:/home/file-center/ 拆分为 D:/ 和 home/file-center/
+                int index = formatted.indexOf('/');
+                home = formatted.substring(0, index + 1);
+                formatted = formatted.substring(index + 1);
+            } else {
+                home = PathTools.formatPath(new File("./").getAbsolutePath());
+            }
+
+            KeyString temp = splitFolderAndName(formatted);
+            String folder = temp.getKey();
+            String name = temp.getValue();
+            return new ClientPath(path, home, null, folder, name, endWithSlash);
+        }
+    }
+
+    protected static class RemotePath extends BasePath implements AutoCloseable {
+
+        private final ChannelSftp channel;
+        private final String oldPwd;
+
+        protected RemotePath(ChannelSftp channel, String oldPwd,
+                String original, String home, String pwd, String folder, String name, boolean endWithSlash) {
+            super(original, home, pwd, folder, name, endWithSlash);
+            this.channel = channel;
+            this.oldPwd = oldPwd;
+            if (oldPwd != null && !"/".equals(oldPwd)) {
+                doChangeToHome(channel);
+            }
+        }
+
+        /** 进入下级路径 **/
+        public RemotePath enter(String path) {
+            boolean endWithSlash = path.charAt(path.length() - 1) == '/';
+            String original = PathTools.concat(this.original, path);
+            KeyString temp = splitFolderAndName(path);
+            String folder = PathTools.concat(this.folder, this.name, temp.getKey());
+            String name = temp.getValue();
+            return new RemotePath(channel, null, original, home, pwd, folder, name, endWithSlash);
+        }
+
+        @Override
+        public void close() {
+            if (oldPwd != null && !"/".equals(oldPwd)) {
+                doChangeDirectory(channel, oldPwd);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder("{")
+                    .append("realPath:").append(effectivePath()).append(",")
+                    .append("original:").append(original).append(",")
+                    .append("absolute:").append(fullPathOnRoot()).append("}")
+                    .toString();
+        }
+
+        public static RemotePath of(ChannelSftp channel, String path) {
+            VerifyTools.requireNotBlank(path, "path");
+
+            String formatted = (".".equals(path) || "./".equals(path)) ? path : PathTools.formatPath(path);
+            boolean isAbsolute = formatted.length() > 0 && formatted.charAt(0) == '/';
+            boolean endWithSlash = formatted.length() > 0 && formatted.charAt(formatted.length() - 1) == '/';
+            String home = doGetHome(channel);
+            String pwd = doGetPwd(channel, home);
+            String pathPwd = isAbsolute ? null : pwd;
+            String oldPwd = isAbsolute ? pwd : null;
+
+            KeyString temp = splitFolderAndName(formatted);
+            String folder = temp.getKey();
+            String name = temp.getValue();
+            return new RemotePath(channel, oldPwd, path, home, pathPwd, folder, name, endWithSlash);
+        }
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..70187b49e7f375d69fe3b0f209e897e786c684ad
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpClient.java
@@ -0,0 +1,159 @@
+package com.gitee.qdbp.tools.sftp;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+import com.gitee.qdbp.able.exception.ServiceException;
+import com.gitee.qdbp.able.result.ResultCode;
+import com.gitee.qdbp.tools.utils.VerifyTools;
+import com.jcraft.jsch.Channel;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+/**
+ * SFTP工具类
+ *
+ * @author zhaohuihua
+ * @version 20230310
+ * @since 5.5.10
+ */
+public class SftpClient {
+
+    private final String host;
+    private final int port;
+    private final String account;
+    private final byte[] password;
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public SftpClient(String host, int port) {
+        this(host, port, null, null);
+    }
+
+    private SftpClient(String host, int port, String account, byte[] password) {
+        VerifyTools.requireNotBlank(host, "host");
+        this.host = host;
+        this.port = port;
+        this.account = account;
+        this.password = password;
+    }
+
+    public SftpChannel connect() {
+        JSch jsch = new JSch();
+        Session session;
+        try {
+            session = jsch.getSession(account, host, port);
+        } catch (JSchException e) {
+            throw new ServiceException(ResultCode.REMOTE_SERVICE_ERROR, e)
+                    .setDetails("sftp --> {}:{}", host, port);
+        }
+
+        if (password != null) {
+            session.setPassword(password);
+        }
+
+        Properties sshConfig = new Properties();
+        // ssh会把每个访问过的主机公钥记录在~/.ssh/known_hosts
+        // 当下次访问相同主机时, OpensSSH会核对公钥
+        // 如果公钥不同, ssh会发出警告, 以避免受到DNSHijack之类的攻击
+        // 设置StrictHostKeyChecking=no关闭该警告
+        sshConfig.put("StrictHostKeyChecking", "no");
+        session.setConfig(sshConfig);
+        try {
+            session.connect();
+        } catch (JSchException e) {
+            session.disconnect();
+            throw new ServiceException(ResultCode.REMOTE_SERVICE_ERROR, e)
+                    .setDetails("sftp --> {}:{}", host, port);
+        }
+
+        Channel channel;
+        try {
+            channel = session.openChannel("sftp");
+        } catch (JSchException e) {
+            session.disconnect();
+            throw new ServiceException(ResultCode.REMOTE_SERVICE_ERROR, e)
+                    .setDetails("sftp --> {}:{}", host, port);
+        }
+        try {
+            channel.connect();
+        } catch (JSchException e) {
+            session.disconnect();
+            channel.disconnect();
+            throw new ServiceException(ResultCode.REMOTE_SERVICE_ERROR, e)
+                    .setDetails("sftp --> {}:{}", host, port);
+        }
+
+        return new SftpChannel(session, (ChannelSftp) channel);
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public String getAccount() { return account; }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder();
+        if (VerifyTools.isNotBlank(account)) {
+            buffer.append(account).append('@');
+        }
+        buffer.append(host);
+        buffer.append(':').append(port);
+        return buffer.toString();
+    }
+
+    public static class Builder {
+
+        private String host;
+        private int port = 22;
+        private String account;
+        private byte[] password;
+
+        protected Builder() {
+        }
+
+        public Builder host(String host) {
+            this.host = host;
+            return this;
+        }
+
+        public Builder port(int port) {
+            this.port = port;
+            return this;
+        }
+
+        public Builder account(String account) {
+            this.account = account;
+            return this;
+        }
+
+        public Builder password(byte[] password) {
+            this.password = password;
+            return this;
+        }
+
+        public Builder password(String password) {
+            if (VerifyTools.isBlank(password)) {
+                this.password = null;
+            } else {
+                this.password = password.getBytes(StandardCharsets.UTF_8);
+            }
+            return this;
+        }
+
+        public SftpClient build() {
+            VerifyTools.requireNotBlank(host, "host");
+            return new SftpClient(host, port, account, password);
+        }
+    }
+}
+
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
new file mode 100644
index 0000000000000000000000000000000000000000..cad1294f4835875c69a58ad8dce363e380ddd588
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/sftp/SftpPath.java
@@ -0,0 +1,308 @@
+package com.gitee.qdbp.tools.sftp;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FilenameFilter;
+import java.io.Serializable;
+import java.nio.file.Path;
+import java.util.Date;
+import com.gitee.qdbp.able.model.file.PathInfo;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * SFTP文件信息
+ *
+ * @author zhaohuihua
+ * @version 20230311
+ * @since 5.5.10
+ */
+class SftpPath extends File implements PathInfo, Serializable {
+
+    private static final long serialVersionUID = 5852748641005853109L;
+
+    private final String fileName;
+    private final String directory;
+    private final String relativePath;
+    private final String absolutePath;
+    private long fileSize;
+    private int permissions;
+    private boolean isFile;
+    private boolean isDirectory;
+    private Date updateTime;
+
+    public SftpPath(String directory, String relativePath) {
+        super(directory, relativePath);
+        this.directory = PathTools.formatPath(directory);
+        this.relativePath = PathTools.formatPath(relativePath);
+        this.absolutePath = PathTools.concat(this.directory, this.relativePath);
+        this.fileName = PathTools.getFileName(StringTools.trimRight(relativePath, '/'));
+    }
+
+    @Override
+    public String getDirectory() {
+        return directory;
+    }
+
+    @Override
+    public String getRelativePath() {
+        return relativePath;
+    }
+
+    @Override
+    public String getName() {
+        return fileName;
+    }
+
+    public int getPermissions() {
+        return permissions;
+    }
+
+    public void setPermissions(int permissions) {
+        this.permissions = permissions;
+    }
+
+    @Override
+    public boolean isFile() {
+        return isFile;
+    }
+
+    protected void setIsFile(boolean isFile) {
+        this.isFile = isFile;
+    }
+
+    @Override
+    public boolean isDirectory() {
+        return isDirectory;
+    }
+
+    protected void setIsDirectory(boolean isDirectory) {
+        this.isDirectory = isDirectory;
+    }
+
+    @Override
+    public long lastModified() {
+        return this.updateTime == null ? -1 : this.updateTime.getTime();
+    }
+
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    protected void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    @Override
+    public String getParent() {
+        String parent = PathTools.concat(true, absolutePath, "..");
+        return parent.startsWith("..") ? null : parent;
+    }
+
+    @Override
+    public SftpPath getParentFile() {
+        return null;
+    }
+
+    @Override
+    public String getPath() {
+        return absolutePath;
+    }
+
+    @Override
+    public boolean isAbsolute() {
+        return true;
+    }
+
+    @Override
+    public String getAbsolutePath() {
+        return absolutePath;
+    }
+
+    @Override
+    public SftpPath getAbsoluteFile() {
+        return this;
+    }
+
+    @Override
+    public String getCanonicalPath() {
+        return absolutePath;
+    }
+
+    @Override
+    public SftpPath getCanonicalFile() {
+        return this;
+    }
+
+    @Override
+    public boolean canRead() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canWrite() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isHidden() {
+        // 输出json时会访问此方法
+        // throw new UnsupportedOperationException();
+        return false;
+    }
+
+    @Override
+    public boolean exists() {
+        return true;
+    }
+
+    @Override
+    public long length() {
+        return this.fileSize;
+    }
+
+    protected void setLength(long fileSize) {
+        this.fileSize = fileSize;
+    }
+
+    @Override
+    public int compareTo(File file) {
+        return this.absolutePath.compareTo(file.getAbsolutePath());
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        return object instanceof SftpPath && compareTo((SftpPath) object) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return this.absolutePath.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return this.absolutePath;
+    }
+
+    @Override
+    public boolean createNewFile() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean delete() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void deleteOnExit() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] list() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] list(FilenameFilter filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public File[] listFiles() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public File[] listFiles(FilenameFilter filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    
+    @Override
+    public File[] listFiles( FileFilter filter) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean mkdir() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean mkdirs() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean renameTo(File dest) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setLastModified(long time) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setReadOnly() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setWritable(boolean writable, boolean ownerOnly) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setWritable(boolean writable) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setReadable(boolean readable, boolean ownerOnly) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setReadable(boolean readable) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setExecutable(boolean executable, boolean ownerOnly) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setExecutable(boolean executable) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canExecute() {
+        return super.canExecute();
+    }
+
+    @Override
+    public long getTotalSpace() {
+        return -1;
+    }
+
+    @Override
+    public long getFreeSpace() {
+        return -1;
+    }
+
+    @Override
+    public long getUsableSpace() {
+        return -1;
+    }
+
+    @Override
+    public Path toPath() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/specialized/KeywordHandler.java b/tools/src/main/java/com/gitee/qdbp/tools/specialized/KeywordHandler.java
index 910a3dff91e06408d005d1ca61fe76cd0f7fb563..4e64d4c527fe06b7c3a366d5d94fa98e7d401f52 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/specialized/KeywordHandler.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/specialized/KeywordHandler.java
@@ -4,12 +4,9 @@ import java.io.IOException;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.TreeSet;
-import com.gitee.qdbp.tools.utils.ConvertTools;
-import com.gitee.qdbp.tools.utils.StringTools;
-import com.gitee.qdbp.tools.utils.VerifyTools;
 import org.lionsoul.jcseg.tokenizer.core.ADictionary;
 import org.lionsoul.jcseg.tokenizer.core.DictionaryFactory;
 import org.lionsoul.jcseg.tokenizer.core.ISegment;
@@ -17,6 +14,9 @@ import org.lionsoul.jcseg.tokenizer.core.IWord;
 import org.lionsoul.jcseg.tokenizer.core.JcsegException;
 import org.lionsoul.jcseg.tokenizer.core.JcsegTaskConfig;
 import org.lionsoul.jcseg.tokenizer.core.SegmentFactory;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+import com.gitee.qdbp.tools.utils.VerifyTools;
 
 /**
  * 关键字收集
@@ -26,7 +26,7 @@ import org.lionsoul.jcseg.tokenizer.core.SegmentFactory;
  */
 public class KeywordHandler {
 
-    private final Set<String> container = new TreeSet<>();
+    private final Set<String> container = new LinkedHashSet<>();
 
     private static final JcsegTaskConfig CONFIG = new JcsegTaskConfig(true);
     private static final ADictionary DICTIONARY = DictionaryFactory.createDefaultDictionary(CONFIG, true);
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlEvaluator.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlEvaluator.java
index 57e4c277fc25945223fd5888ecbc08eb8fa818fc..88f575e9b13a701f8fe42c35ddbb5a589b091cf6 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlEvaluator.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlEvaluator.java
@@ -1,10 +1,7 @@
 package com.gitee.qdbp.tools.utils;
 
-import java.util.HashMap;
 import java.util.Map;
-import com.gitee.qdbp.able.exception.FormatException;
-import com.gitee.qdbp.able.model.reusable.ExpressionContext;
-import com.gitee.qdbp.able.model.reusable.StackWrapper;
+import com.gitee.qdbp.able.model.reusable.ExpressionExecutor;
 
 /**
  * Ognl计算处理类<br>
@@ -14,96 +11,10 @@ import com.gitee.qdbp.able.model.reusable.StackWrapper;
  * @author zhaohuihua
  * @version 20210808
  */
-public class OgnlEvaluator extends ExpressionContext {
+public class OgnlEvaluator extends ExpressionExecutor {
 
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression) throws FormatException {
-        return doEvaluate(expression, OgnlTools.RESULT_SCALE, OgnlTools.CALC_SCALE, null);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression, Map<String, Object> root) throws FormatException {
-        return doEvaluate(expression, OgnlTools.RESULT_SCALE, OgnlTools.CALC_SCALE, root);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param scale 结果保留几位小数 (只在结果是数字时生效)
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression, int scale, Map<String, Object> root) throws FormatException {
-        return doEvaluate(expression, scale, OgnlTools.CALC_SCALE, root);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param resultScale 结果保留几位小数 (只在结果是数字时生效)
-     * @param calcScale 计算过程中的数字保留几位小数
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression, int resultScale, int calcScale, Map<String, Object> root)
-            throws FormatException {
-        return doEvaluate(expression, resultScale, calcScale, root);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param useBigDecimal 是否转换为BigDecimal以解决浮点数计算精度的问题
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    public Object evaluate(String expression, boolean useBigDecimal, Map<String, Object> root) throws FormatException {
-        return doEvaluate(expression, null, useBigDecimal ? OgnlTools.CALC_SCALE : null, root);
-    }
-
-    /**
-     * 计算Ognl表达式的值
-     * 
-     * @param expression 表达式
-     * @param resultScale 结果保留几位小数 (只在结果是数字时生效)
-     * @param calcScale 计算过程中的数字保留几位小数
-     * @param root 变量根容器
-     * @return 计算结果
-     * @throws FormatException 表达式语法错误, 表达式取值错误
-     */
-    protected Object doEvaluate(String expression, Integer resultScale, Integer calcScale, Map<String, Object> root)
-            throws FormatException {
-        // 替换静态方法及自定义方法名
-        String newExpression = replaceCustomizedFunctions(expression);
-        Map<String, Object> map = new HashMap<>();
-        if (root != null && !root.isEmpty()) {
-            map.putAll(root);
-        }
-
-        // 将值栈自身以ThisStack为名保存至栈中, 以实现在业务侧自定义值栈自身的函数, 如@contains(user.name)
-        // BaseContext.parseStackFunction将@contains(user.name)替换为ThisStack.contains('user.name')
-        StackWrapper thisStack = createStackWrapperInstance(map);
-        map.put("ThisStack", thisStack);
-
-        return OgnlTools.doEvaluate(newExpression, resultScale, calcScale, map);
+    @Override
+    protected Object evaluate(String expression, Integer resultScale, Integer calcScale, Map<String, Object> root) {
+        return OgnlTools.doEvaluate(expression, resultScale, calcScale, root);
     }
 }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlTools.java
index 32125781656dcb9164b9bd6eea48d298115e2dd9..51f18d637f869f2b0fe7aa1e364286d04d7757c4 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/OgnlTools.java
@@ -1,13 +1,10 @@
 package com.gitee.qdbp.tools.utils;
 
-import java.lang.reflect.Array;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import com.gitee.qdbp.able.exception.FormatException;
+import com.gitee.qdbp.able.model.reusable.ExpressionExecutor;
+import com.gitee.qdbp.able.model.reusable.ExpressionMap;
 import ognl.ExpressionSyntaxException;
 import ognl.InappropriateExpressionException;
 import ognl.MethodFailedException;
@@ -112,14 +109,18 @@ public class OgnlTools {
         }
 
         Object vars = root;
-        if (root != null && calcScale != null) {
-            vars = convertFieldToBigDecimal(root, calcScale);
+        if (root != null && calcScale != null && !(root instanceof ExpressionMap)) {
+            Map<String, Object> json = JsonTools.beanToMap(root);
+            ExpressionMap map = new ExpressionMap();
+            map.setDecimalCalcScale(calcScale);
+            map.putAll(json);
+            vars = map;
         }
 
         OgnlContext context = new OgnlContext(null, null, OgnlMemberAccess.ALLOW_ALL_ACCESS);
         try {
             Object result = Ognl.getValue(expression, context, vars);
-            return setResultScale(result, resultScale);
+            return ExpressionExecutor.trySetDecimalScale(result, resultScale);
         } catch (ExpressionSyntaxException e) {
             throw new FormatException("ExpressionSyntaxException: " + expression, e);
         } catch (MethodFailedException e) {
@@ -139,88 +140,6 @@ public class OgnlTools {
         }
     }
 
-    private static Object setResultScale(Object result, Integer scale) {
-        if (result instanceof BigDecimal) {
-            BigDecimal number = (BigDecimal) result;
-            if (scale == null) {
-                return clearTrailingZeros(number);
-            } else if (number.scale() > scale) {
-                return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
-            }
-        } else if (result instanceof Double) {
-            BigDecimal number = new BigDecimal(String.valueOf(result));
-            if (scale == null) {
-                return clearTrailingZeros(number);
-            } else if (number.scale() > scale) {
-                return number.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
-            }
-        } else if (result instanceof Float) {
-            BigDecimal number = new BigDecimal(String.valueOf(result));
-            if (scale == null) {
-                return clearTrailingZeros(number);
-            } else if (number.scale() > scale) {
-                return number.setScale(scale, BigDecimal.ROUND_HALF_UP).floatValue();
-            }
-        }
-        return result;
-    }
-
-    private static BigDecimal clearTrailingZeros(BigDecimal number) {
-        number = number.stripTrailingZeros();
-        // new BigDecimal(20000).stripTrailingZeros(); = 2E+4 (科学计数法)
-        // 此时precision=1,scale=-4; 需要将scale设置为0
-        if (number.scale() < 0) {
-            number = number.setScale(0, BigDecimal.ROUND_HALF_UP);
-        }
-        return number;
-    }
-
-    private static Object convertFieldToBigDecimal(Object object, int scale) {
-        if (object == null) {
-            return null;
-        }
-        if (object instanceof BigDecimal) {
-            BigDecimal number = (BigDecimal) object;
-            if (number.scale() >= scale) {
-                return number;
-            } else {
-                return number.setScale(scale, BigDecimal.ROUND_HALF_UP);
-            }
-        } else if (object instanceof BigInteger) {
-            return new BigDecimal(object.toString()).setScale(scale, BigDecimal.ROUND_HALF_UP);
-        } else if (object instanceof Number) {
-            return new BigDecimal(String.valueOf(object)).setScale(scale, BigDecimal.ROUND_HALF_UP);
-        } else if (ReflectTools.isPrimitive(object.getClass(), false)) {
-            return object;
-        } else if (object instanceof Map) {
-            Map<?, ?> map = (Map<?, ?>) object;
-            Map<Object, Object> result = new HashMap<>();
-            for (Map.Entry<?, ?> entry : map.entrySet()) {
-                result.put(entry.getKey(), convertFieldToBigDecimal(entry.getValue(), scale));
-            }
-            return result;
-        } else if (object.getClass().isArray()) {
-            List<Object> result = new ArrayList<>();
-            int size = Array.getLength(object);
-            for (int i = 0; i < size; i++) {
-                Object item = Array.get(object, i);
-                result.add(convertFieldToBigDecimal(item, scale));
-            }
-            return result.toArray();
-        } else if (object instanceof Iterable) {
-            Iterable<?> iterable = (Iterable<?>) object;
-            List<Object> result = new ArrayList<>();
-            for (Object item : iterable) {
-                result.add(convertFieldToBigDecimal(item, scale));
-            }
-            return result;
-        } else {
-            // 普通对象不能自动转换为map, 因为EL表达式中有可能会调用对象的方法
-            // Map<String, Object> map = JsonTools.beanToMap(object, true, false);
-            return object;
-        }
-    }
-
     /**
      * 从容器中获取Ognl表达式的值
      *
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/QueryTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/QueryTools.java
index d353a7746f373e7fa46cdae7c4657fcf97acb573..c52ef1fb7bfd6a242289fd8b08602df9baf6c6ce 100644
--- a/tools/src/main/java/com/gitee/qdbp/tools/utils/QueryTools.java
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/QueryTools.java
@@ -7,7 +7,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.regex.Pattern;
-import com.alibaba.fastjson.util.TypeUtils;
 import com.gitee.qdbp.able.jdbc.ordering.Orderings;
 import com.gitee.qdbp.able.jdbc.paging.PageList;
 import com.gitee.qdbp.able.jdbc.paging.Paging;
@@ -125,7 +124,6 @@ public class QueryTools {
         }
     }
 
-    private static final Pattern NUMBER = Pattern.compile("^[+\\-]?[\\d,]*(\\.\\d+)?$");
     private static final Pattern DATE = Pattern.compile("^[\\d\\-:. ]+$");
 
     /**
@@ -211,8 +209,11 @@ public class QueryTools {
                 }
                 Object realActualValue = actualValue; // 只支持Date
                 try {
-                    if (actualValue instanceof CharSequence && DATE.matcher(actualValue.toString()).matches()) {
-                        realActualValue = TypeUtils.castToDate(actualValue);
+                    if (actualValue instanceof CharSequence) {
+                        String string = actualValue.toString();
+                        if (DATE.matcher(string).matches()) {
+                            realActualValue = DateTools.parse(string);
+                        }
                     }
                 } catch (Exception e) {
                     continue;
@@ -231,7 +232,7 @@ public class QueryTools {
                 Object realActualValue = actualValue; // 只支持Date
                 try {
                     if (actualValue instanceof CharSequence && DATE.matcher(actualValue.toString()).matches()) {
-                        realActualValue = TypeUtils.castToDate(actualValue);
+                        realActualValue = DateTools.parse(actualValue.toString());
                     }
                 } catch (Exception e) {
                     continue;
@@ -318,10 +319,11 @@ public class QueryTools {
 
     private static Object convertToNumberOrDate(Object actualValue) {
         if (actualValue instanceof CharSequence) {
-            if (NUMBER.matcher(actualValue.toString()).matches()) {
-                return TypeUtils.castToDouble(actualValue);
-            } else if (DATE.matcher(actualValue.toString()).matches()) {
-                return TypeUtils.castToDate(actualValue);
+            String string = actualValue.toString();
+            if (StringTools.isNumber(string)) {
+                return ConvertTools.toNumber(string);
+            } else if (DATE.matcher(string).matches()) {
+                return DateTools.parse(string);
             }
         }
         return actualValue;
@@ -433,8 +435,8 @@ public class QueryTools {
         } else {
             try {
                 double actual = actualValue.doubleValue();
-                Number minNumber = VerifyTools.isBlank(minValue) ? null : TypeUtils.castToDouble(minValue);
-                Number maxNumber = VerifyTools.isBlank(maxValue) ? null : TypeUtils.castToDouble(maxValue);
+                Number minNumber = VerifyTools.isBlank(minValue) ? null : JsonTools.convert(minValue, Double.class);
+                Number maxNumber = VerifyTools.isBlank(maxValue) ? null : JsonTools.convert(maxValue, Double.class);
                 if (minNumber != null && maxNumber != null) {
                     return actual >= minNumber.doubleValue() && actual < maxNumber.doubleValue();
                 } else if (maxNumber != null) {
@@ -467,8 +469,8 @@ public class QueryTools {
         } else {
             try {
                 long actual = actualValue.getTime();
-                Date minDate = VerifyTools.isBlank(minValue) ? null : TypeUtils.castToDate(minValue);
-                Date maxDate = VerifyTools.isBlank(maxValue) ? null : TypeUtils.castToDate(maxValue);
+                Date minDate = VerifyTools.isBlank(minValue) ? null : JsonTools.convert(minValue, Date.class);
+                Date maxDate = VerifyTools.isBlank(maxValue) ? null : JsonTools.convert(maxValue, Date.class);
                 if (minDate != null && maxDate != null) {
                     return actual >= minDate.getTime() && actual < maxDate.getTime();
                 } else if (maxDate != null) {
@@ -631,7 +633,7 @@ public class QueryTools {
             return false;
         } else {
             double actualNumber = actualValue.getTime();
-            double expectNumber = TypeUtils.castToDate(expectValue).getTime();
+            double expectNumber = JsonTools.convert(expectValue, Date.class).getTime();
             return judgeValue(actualNumber, flag, expectNumber);
         }
     }
@@ -650,7 +652,7 @@ public class QueryTools {
             return false;
         } else {
             double actualNumber = actualValue.getTime();
-            Date expectDate = TypeUtils.castToDate(expectValue);
+            Date expectDate = JsonTools.convert(expectValue, Date.class);
             double expectNumber = DateTools.toStartTime(expectDate).getTime();
             return judgeValue(actualNumber, flag, expectNumber);
         }
@@ -670,7 +672,7 @@ public class QueryTools {
             return false;
         } else {
             double actualNumber = actualValue.getTime();
-            Date expectDate = TypeUtils.castToDate(expectValue);
+            Date expectDate = JsonTools.convert(expectValue, Date.class);
             double expectNumber = DateTools.toEndTime(expectDate).getTime();
             return judgeValue(actualNumber, flag, expectNumber);
         }
@@ -690,7 +692,7 @@ public class QueryTools {
             return false;
         } else {
             double actualNumber = actualValue.doubleValue();
-            double expectNumber = TypeUtils.castToDouble(expectValue);
+            double expectNumber = JsonTools.convert(expectValue, Double.class);
             return judgeValue(actualNumber, flag, expectNumber);
         }
     }
diff --git a/tools/src/main/java/com/gitee/qdbp/tools/utils/YamlTools.java b/tools/src/main/java/com/gitee/qdbp/tools/utils/YamlTools.java
new file mode 100644
index 0000000000000000000000000000000000000000..e86d5590ae00e6c61fe3973d6489a6a8f5e8ecda
--- /dev/null
+++ b/tools/src/main/java/com/gitee/qdbp/tools/utils/YamlTools.java
@@ -0,0 +1,68 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Yaml工具类
+ *
+ * @author zhaohuihua
+ * @version 20230118
+ */
+public class YamlTools {
+
+    /** 静态工具类私有构造方法 **/
+    private YamlTools() {
+    }
+
+    /** 将Java对象转换为Yaml字符串 **/
+    public static String toYamlString(Object object) {
+        Yaml yaml = createYaml();
+        return yaml.dump(object);
+    }
+
+    /** 将Yaml字符串解析为Map对象 **/
+    public static Map<String, Object> parseAsMap(String string) {
+        Yaml yaml = createYaml();
+        Object result = yaml.load(string);
+        return JsonTools.beanToMap(result, false, false);
+    }
+
+    /** 将Yaml字符串解析为Map数组 **/
+    public static List<Map<String, Object>> parseAsMaps(String string) {
+        Yaml yaml = createYaml();
+        Object object = yaml.load(string);
+        List<Object> list = ConvertTools.parseList(object);
+        return JsonTools.beanToMaps(list, false, false);
+    }
+
+    /** 将Yaml字符串解析为Java对象 **/
+    public static <T> T parseAsObject(String string, Class<T> clazz) {
+        Yaml yaml = createYaml();
+        return yaml.loadAs(string, clazz);
+    }
+
+    /** 将Yaml字符串解析为Java对象列表 **/
+    public static <T> List<T> parseAsObjects(String string, Class<T> clazz) {
+        Yaml yaml = createYaml();
+        Object object = yaml.load(string);
+        List<Object> list = ConvertTools.parseList(object);
+        List<T> results = new ArrayList<>();
+        for (Object item : list) {
+            results.add(JsonTools.convert(item, clazz));
+        }
+        return results;
+    }
+
+    private static Yaml createYaml() {
+        LoaderOptions options = new LoaderOptions();
+        options.setAllowDuplicateKeys(false);
+        options.setAllowRecursiveKeys(true);
+        // spring boot 是这么设置的
+        options.setMaxAliasesForCollections(Integer.MAX_VALUE);
+        return new Yaml(options);
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java b/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..30ed7807369841006827c9e2de3b6fb209845b5e
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/able/beans/TreeNodesTest.java
@@ -0,0 +1,141 @@
+package com.gitee.qdbp.able.beans;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.able.function.BaseFunction;
+import com.gitee.qdbp.tools.codec.CodeTools;
+import com.gitee.qdbp.tools.utils.AssertTools;
+import com.gitee.qdbp.tools.utils.StringTools;
+
+/**
+ * 树形节点测试类
+ *
+ * @author zhaohuihua
+ * @version 20220813
+ */
+@Test
+public class TreeNodesTest {
+
+    private static final CodeTools codeTools = new CodeTools(1, 2);
+
+    public void test() {
+        List<String> list = generateTestData();
+
+        TreeNodes<String> container = new TreeNodes<>(list, true, new KeyGetter(), new ParentGetter());
+
+        List<String> rootElements = container.getRootElements();
+        AssertTools.assertDeepEquals(rootElements, Arrays.asList("T01", "T02", "T03"));
+
+        List<String> t01Children = container.findChildElements("T02");
+        AssertTools.assertDeepEquals(t01Children, Arrays.asList("T0201", "T0202", "T0203"));
+
+        List<String> t020304Children = container.findChildElements("T020304");
+        Assert.assertEquals(t020304Children.size(), 0);
+
+        List<String> t020304Ancestors = container.findAllAncestorElements("T020304");
+        AssertTools.assertDeepEquals(t020304Ancestors, Arrays.asList("T02", "T0203"));
+
+        List<String> t02Descendants = container.findAllDescendantElements("T02");
+        AssertTools.assertDeepEquals(t02Descendants, Arrays.asList(
+                "T0201", "T020101", "T020102", "T020103", "T020104",
+                "T0202", "T020201", "T020202", "T020203", "T020204",
+                "T0203", "T020301", "T020302", "T020303", "T020304"));
+
+        { // 深度优先遍历
+            TreeNodes.Node<String> t01Node = container.findNode("T01");
+            Iterator<TreeNodes.Node<String>> t01NodeIterator = t01Node.depthFirstNodeIterator();
+            while (t01NodeIterator.hasNext()) {
+                TreeNodes.Node<String> next = t01NodeIterator.next();
+                String element = next.getElement();
+                if (element.endsWith("04")) {
+                    next.getParentNode().addChildElement(StringTools.replaceSuffix(element, "05"));
+                }
+            }
+            List<String> t01Descendants = t01Node.getAllDescendantElements();
+            AssertTools.assertDeepEquals(t01Descendants, Arrays.asList(
+                    "T0101", "T010101", "T010102", "T010103", "T010104", "T010105",
+                    "T0102", "T010201", "T010202", "T010203", "T010204", "T010205",
+                    "T0103", "T010301", "T010302", "T010303", "T010304", "T010305"));
+
+            Iterator<TreeNodes.Node<String>> t01NodeIterator2 = t01Node.depthFirstNodeIterator();
+            while (t01NodeIterator2.hasNext()) {
+                TreeNodes.Node<String> next = t01NodeIterator2.next();
+                String element = next.getElement();
+                if (element.contains("02")) {
+                    t01NodeIterator2.remove();
+                }
+            }
+            List<String> t01Descendants2 = t01Node.getAllDescendantElements();
+            AssertTools.assertDeepEquals(t01Descendants2, Arrays.asList(
+                    "T0101", "T010101", "T010103", "T010104", "T010105",
+                    "T0103", "T010301", "T010303", "T010304", "T010305"));
+        }
+
+        { // 广度优先遍历
+            TreeNodes.Node<String> t03Node = container.findNode("T03");
+            Iterator<TreeNodes.Node<String>> t03NodeIterator = t03Node.depthFirstNodeIterator();
+            while (t03NodeIterator.hasNext()) {
+                TreeNodes.Node<String> next = t03NodeIterator.next();
+                String element = next.getElement();
+                if (element.endsWith("04")) {
+                    next.getParentNode().addChildElement(StringTools.replaceSuffix(element, "05"));
+                }
+            }
+            List<String> t03Descendants = t03Node.getAllDescendantElements();
+            AssertTools.assertDeepEquals(t03Descendants, Arrays.asList(
+                    "T0301", "T030101", "T030102", "T030103", "T030104", "T030105",
+                    "T0302", "T030201", "T030202", "T030203", "T030204", "T030205",
+                    "T0303", "T030301", "T030302", "T030303", "T030304", "T030305"));
+
+            Iterator<TreeNodes.Node<String>> t03NodeIterator2 = t03Node.depthFirstNodeIterator();
+            while (t03NodeIterator2.hasNext()) {
+                TreeNodes.Node<String> next = t03NodeIterator2.next();
+                String element = next.getElement();
+                if (element.contains("02")) {
+                    t03NodeIterator2.remove();
+                }
+            }
+            List<String> t03Descendants2 = t03Node.getAllDescendantElements();
+            AssertTools.assertDeepEquals(t03Descendants2, Arrays.asList(
+                    "T0301", "T030101", "T030103", "T030104", "T030105",
+                    "T0303", "T030301", "T030303", "T030304", "T030305"));
+        }
+    }
+
+    private List<String> generateTestData() {
+        List<String> list = new ArrayList<>();
+        for (int i = 1; i <= 3; i++) {
+            String si = codeTools.pad("T", i);
+            list.add(si);
+            for (int j = 1; j <= 3; j++) {
+                String sj = codeTools.pad(si, j);
+                list.add(sj);
+                for (int k = 1; k <= 4; k++) {
+                    String sk = codeTools.pad(sj, k);
+                    list.add(sk);
+                }
+            }
+        }
+        return list;
+    }
+
+    private static class KeyGetter implements BaseFunction<String, String> {
+
+        @Override
+        public String apply(String s) {
+            return s;
+        }
+    }
+
+    private static class ParentGetter implements BaseFunction<String, String> {
+
+        @Override
+        public String apply(String s) {
+            return codeTools.parent(s);
+        }
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..46e88ddbb44c46d4d44903d47dae62808fc62ca6
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/able/matches/StringExtractorTest.java
@@ -0,0 +1,71 @@
+package com.gitee.qdbp.able.matches;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+
+/**
+ * 字符串提取测试类
+ *
+ * @author zhaohuihua
+ * @version 20220112
+ */
+@Test
+public class StringExtractorTest {
+
+    @Test
+    public void testStringExtractor() {
+        RegexpStringExtractor extractor = new RegexpStringExtractor("[+-]?(\\d+)", 1);
+        String string = "aabbcc+1122ddee-33ff44";
+        LocatedSubString first = extractor.extractFirst(string);
+        System.out.println(first);
+        Assert.assertEquals(first.getGroupContent(), "1122");
+        Assert.assertEquals(first.getGroupStartIndex(), 7);
+        Assert.assertEquals(first.getGroupEndIndex(), 11);
+        Assert.assertEquals(first.getMatchContent(), "+1122");
+        Assert.assertEquals(first.getMatchStartIndex(), 6);
+        Assert.assertEquals(first.getMatchEndIndex(), 11);
+        System.out.println("------------");
+        LocatedSubStrings list = extractor.extractAll(string);
+        System.out.println(ConvertTools.joinToString(list, '\n'));
+        Assert.assertEquals(list.size(), 3);
+        LocatedSubString second = list.get(1);
+        Assert.assertEquals(second.getGroupContent(), "33");
+        Assert.assertEquals(second.getGroupStartIndex(), 16);
+        Assert.assertEquals(second.getGroupEndIndex(), 18);
+        Assert.assertEquals(second.getMatchContent(), "-33");
+        Assert.assertEquals(second.getMatchStartIndex(), 15);
+        Assert.assertEquals(second.getMatchEndIndex(), 18);
+    }
+
+    @Test
+    public void testGroupExtractor() {
+        RegexpGroupExtractor extractor = new RegexpGroupExtractor("([+-])?(\\d+)", 1, 2);
+        String string = "aabbcc+1122ddee-33ff44";
+        GroupSubString first = extractor.extractFirst(string);
+        System.out.println(first);
+        Assert.assertEquals(first.get(1).getContent(), "+");
+        Assert.assertEquals(first.get(1).getStartIndex(), 6);
+        Assert.assertEquals(first.get(1).getEndIndex(), 7);
+        Assert.assertEquals(first.get(2).getContent(), "1122");
+        Assert.assertEquals(first.get(2).getStartIndex(), 7);
+        Assert.assertEquals(first.get(2).getEndIndex(), 11);
+        Assert.assertEquals(first.getMatchContent(), "+1122");
+        Assert.assertEquals(first.getMatchStartIndex(), 6);
+        Assert.assertEquals(first.getMatchEndIndex(), 11);
+        System.out.println("------------");
+        GroupSubStrings list = extractor.extractAll(string);
+        System.out.println(ConvertTools.joinToString(list, '\n'));
+        Assert.assertEquals(list.size(), 3);
+        GroupSubString second = list.get(1);
+        Assert.assertEquals(second.get(1).getContent(), "-");
+        Assert.assertEquals(second.get(1).getStartIndex(), 15);
+        Assert.assertEquals(second.get(1).getEndIndex(), 16);
+        Assert.assertEquals(second.get(2).getContent(), "33");
+        Assert.assertEquals(second.get(2).getStartIndex(), 16);
+        Assert.assertEquals(second.get(2).getEndIndex(), 18);
+        Assert.assertEquals(second.getMatchContent(), "-33");
+        Assert.assertEquals(second.getMatchStartIndex(), 15);
+        Assert.assertEquals(second.getMatchEndIndex(), 18);
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/able/matches/WrapStringMatcherTest.java b/tools/src/test/java/com/gitee/qdbp/able/matches/WrapStringMatcherTest.java
index 2932e96f509f0d573f68dcf7a766b0b9f3440b16..bb6abcb0d3ca6b602b70c9f97d9477bca4d048ad 100644
--- a/tools/src/test/java/com/gitee/qdbp/able/matches/WrapStringMatcherTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/able/matches/WrapStringMatcherTest.java
@@ -1,55 +1,75 @@
 package com.gitee.qdbp.able.matches;
 
+import java.util.ArrayList;
+import java.util.List;
+import org.testng.annotations.Test;
 import com.gitee.qdbp.able.matches.StringMatcher.LogicType;
+import com.gitee.qdbp.tools.utils.AssertTools;
 import com.gitee.qdbp.tools.utils.StringTools;
 
+@Test
 public class WrapStringMatcherTest {
 
-    public static void main(String[] args) {
-        // @formatter:off
-        String[] sources = new String[] {
+    private static final String[] sources = new String[] {
             "/home/files/202005/aaa.docx",
             "/home/files/202005/bbb.html",
             "/home/files/202005/aaa.docx.bak",
             "/home/files/temp/202005/aaa.docx",
             "/home/files/temp/202005/bbb.html",
             "/home/files/temp/202005/aaa.docx.bak"
-        };
-        // @formatter:on
-        test1(sources);
-        test2(sources);
-        test3(sources);
-    }
+    };
 
-    private static void test1(String[] sources) {
+    @Test
+    public void test1() {
         // 不在temp文件夹下的docx文件
         String rule1 = "contains!:/temp/";
         String rule2 = "ant:/**/*.docx";
         StringMatcher matcher = new WrapStringMatcher(LogicType.AND, rule1, rule2);
-        doMatches(matcher, sources);
+        List<Boolean> result = doMatches(matcher, sources);
+        assertEquals(result, "1,0,0,0,0,0");
     }
 
-    private static void test2(String[] sources) {
+    @Test
+    public void test2() {
         // temp文件夹下的bak文件
         String rule1 = "contains:/temp/";
         String rule2 = "ant:/**/*.bak";
         StringMatcher matcher = new WrapStringMatcher(LogicType.AND, rule1, rule2);
-        doMatches(matcher, sources);
+        List<Boolean> result = doMatches(matcher, sources);
+        assertEquals(result, "0,0,0,0,0,1");
     }
 
-    private static void test3(String[] sources) {
+    @Test
+    public void test3() {
         // temp文件或bak文件
         String rule1 = "contains:/temp/";
         String rule2 = "ant:/**/*.bak";
         StringMatcher matcher = new WrapStringMatcher(LogicType.OR, rule1, rule2);
-        doMatches(matcher, sources);
+        List<Boolean> result = doMatches(matcher, sources);
+        assertEquals(result, "0,0,1,1,1,1");
+    }
+
+    private static void assertEquals(List<Boolean> actual, String expected) {
+        List<Boolean> list = new ArrayList<>();
+        List<String> strings = StringTools.splits(expected, ',');
+        for (String item : strings) {
+            if ("0".equals(item)) {
+                list.add(false);
+            } else {
+                list.add(true);
+            }
+        }
+        AssertTools.assertDeepEquals(actual, list);
     }
 
-    private static void doMatches(StringMatcher matcher, String... strings) {
+    private static List<Boolean> doMatches(StringMatcher matcher, String... strings) {
         System.out.println(matcher.toString());
+        List<Boolean> result = new ArrayList<>();
         for (String string : strings) {
             boolean matches = matcher.matches(string);
             System.out.println(StringTools.pad(String.valueOf(matches), ' ', false, 8) + string);
+            result.add(matches);
         }
+        return result;
     }
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/matches/StringMatcherParserTest.java b/tools/src/test/java/com/gitee/qdbp/matches/StringMatcherParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4eb1a2fc7c8fdf7db49cc4dbdfb5829b2cda47c
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/matches/StringMatcherParserTest.java
@@ -0,0 +1,25 @@
+package com.gitee.qdbp.matches;
+
+import org.testng.annotations.Test;
+import com.gitee.qdbp.able.matches.StringMatcher;
+import com.gitee.qdbp.able.matches.WrapStringMatcher;
+
+/**
+ * StringMatcherParserTest
+ *
+ * @author zhaohuihua
+ * @version 20220206
+ */
+@Test
+public class StringMatcherParserTest {
+
+    @Test
+    public void test() {
+        String rules = "中国货币网.{0,5}上海清算所网站(向.{0,10})?披露\n" +
+                "上海清算所(网站)?.{0,5}中国货币网(向.{0,10})?披露\n" +
+                "上海清算所(网站)?(向.{0,5})?披露\n" +
+                "中国货币网(向.{0,10})?披露";
+        StringMatcher matcher = WrapStringMatcher.parseMatchers(rules, StringMatcher.LogicType.OR, "regexp");
+        System.out.println(matcher.toString());
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/matches/StringReplacerParserTest.java b/tools/src/test/java/com/gitee/qdbp/matches/StringReplacerParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..67ab00aa25b4c92db67ea2f7f0e4dde7134cecff
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/matches/StringReplacerParserTest.java
@@ -0,0 +1,56 @@
+package com.gitee.qdbp.matches;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.able.beans.KeyString;
+import com.gitee.qdbp.able.matches.StringReplacer;
+import com.gitee.qdbp.able.matches.WrapStringReplacer;
+import com.gitee.qdbp.tools.files.PathTools;
+
+/**
+ * StringMatcherParserTest
+ *
+ * @author zhaohuihua
+ * @version 20220206
+ */
+@Test
+public class StringReplacerParserTest {
+
+    private static final Logger log = LoggerFactory.getLogger(StringReplacerParserTest.class);
+
+    @Test
+    public void test() {
+        List<KeyString> sources = new ArrayList<>();
+        sources.add(new KeyString("负债和所有者权益", "负债及所有者权益"));
+        sources.add(new KeyString("分配股利、利润或偿付利息支付的现金", "分配股利、利润或偿付利息所支付的现金"));
+        sources.add(new KeyString("年初现金及现金等价物余额", "期初现金及现金等价物余额"));
+        sources.add(new KeyString("年末现金及现金等价物余额", "期末现金及现金等价物余额"));
+        sources.add(new KeyString("盈余公积金", "盈余公积"));
+        sources.add(new KeyString("资本公积", "资本公积"));
+        StringReplacer replacer = WrapStringReplacer.parseReplacers(getReplaceRules());
+        System.out.println(replacer.toString());
+        System.out.println();
+        for (KeyString source : sources) {
+            String target = replacer.replace(source.getKey());
+            System.out.println(source + "\t\t" + target);
+            Assert.assertEquals(target, source.getValue());
+        }
+    }
+
+    private static String getReplaceRules() {
+        String filePath = "replace-rules.txt";
+        URL url = PathTools.findClassResource(StringReplacerParserTest.class, filePath);
+        try {
+            return PathTools.downloadString(url);
+        } catch (IOException e) {
+            log.error("FailedToReadTitleClearSymbols", e);
+            return null;
+        }
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/matches/replace-rules.txt b/tools/src/test/java/com/gitee/qdbp/matches/replace-rules.txt
new file mode 100644
index 0000000000000000000000000000000000000000..04e7056d1c15365c222f4b87ddda8c1d5f1cf469
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/matches/replace-rules.txt
@@ -0,0 +1,7 @@
+负债和所有者权益 -- 负债及所有者权益
+// 分配股利、利润或偿付利息【所】支付的现金有时缺少“所”字
+(?<!所)支付的现金 -- 所支付的现金
+// 年初现金替换为期初现金
+年([初末])现金 -- 期$1现金
+// 盈余公积金替换为盈余公积
+(盈余|资本)公积金 -- $1公积
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java b/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
index 4482eac8189518fe23def7515e6d65adcfe7600c..1422aec716041ed9ee90631aa04fbda022268241 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/compare/CompareTest.java
@@ -1,10 +1,13 @@
 package com.gitee.qdbp.tools.compare;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import org.testng.Assert;
 import org.testng.annotations.Test;
+import com.gitee.qdbp.able.function.BaseFunction;
 import com.gitee.qdbp.tools.utils.AssertTools;
 
 /**
@@ -18,7 +21,7 @@ public class CompareTest {
 
     @Test(priority = 1)
     public void testAscAndNullsToLast() {
-        List<Integer> list = Arrays.asList(3,7,2,3,9,6,null,1,3,0,2,7);
+        List<Integer> list = Arrays.asList(3, 7, 2, 3, 9, 6, null, 1, 3, 0, 2, 7);
         Collections.sort(list, new Comparator<Integer>() {
             public int compare(Integer a, Integer b) {
                 return CompareTools.ascCompare(a, b);
@@ -31,7 +34,7 @@ public class CompareTest {
 
     @Test(priority = 2)
     public void testAscAndNullsToFirst() {
-        List<Integer> list = Arrays.asList(3,7,2,3,9,6,null,1,3,0,2,7);
+        List<Integer> list = Arrays.asList(3, 7, 2, 3, 9, 6, null, 1, 3, 0, 2, 7);
         Collections.sort(list, new Comparator<Integer>() {
             public int compare(Integer a, Integer b) {
                 return CompareTools.ascCompare(a, b, true);
@@ -44,7 +47,7 @@ public class CompareTest {
 
     @Test(priority = 3)
     public void testDescAndNullsToLast() {
-        List<Integer> list = Arrays.asList(3,7,2,3,9,6,null,1,3,0,2,7);
+        List<Integer> list = Arrays.asList(3, 7, 2, 3, 9, 6, null, 1, 3, 0, 2, 7);
         Collections.sort(list, new Comparator<Integer>() {
             public int compare(Integer a, Integer b) {
                 return CompareTools.descCompare(a, b);
@@ -57,7 +60,7 @@ public class CompareTest {
 
     @Test(priority = 4)
     public void testDescAndNullsToFirst() {
-        List<Integer> list = Arrays.asList(3,7,2,3,9,6,null,1,3,0,2,7);
+        List<Integer> list = Arrays.asList(3, 7, 2, 3, 9, 6, null, 1, 3, 0, 2, 7);
         Collections.sort(list, new Comparator<Integer>() {
             public int compare(Integer a, Integer b) {
                 return CompareTools.descCompare(a, b, true);
@@ -67,4 +70,117 @@ public class CompareTest {
         System.out.println(list);
         AssertTools.assertDeepEquals(list, Arrays.asList(null, 9, 7, 7, 6, 3, 3, 3, 2, 2, 1, 0));
     }
+
+    @Test(priority = 5)
+    public void testObjects() {
+        List<Item1> items = new ArrayList<>();
+        items.add(new Item1(10, 15));
+        items.add(new Item2(null, 8));
+        items.add(new Item2(5, null));
+        items.add(new Item1(10, 13));
+        items.add(new Item2(5, 8));
+        items.add(new Item1(10, null));
+        items.add(new Item1(10, 8));
+        Collections.sort(items);
+        System.out.println(items);
+    }
+
+    @Test(priority = 6)
+    public void testObjects2() {
+        List<Item1> items = new ArrayList<>();
+        items.add(new Item1(10, 15));
+        items.add(new Item2(null, 8));
+        items.add(new Item2(5, null));
+        items.add(new Item1(10, 13));
+        items.add(new Item2(5, 8));
+        items.add(new Item1(10, null));
+        items.add(new Item1(10, 8));
+        Collections.sort(items, new Comparator<Item1>() {
+            public int compare(Item1 a, Item1 b) {
+                return CompareTools.stream()
+                        .asc(a, b, new BaseFunction<Item1, Integer>() {
+                            public Integer apply(Item1 o) {
+                                return o.start;
+                            }
+                        })
+                        .asc(a, b, new BaseFunction<Item1, Integer>() {
+                            public Integer apply(Item1 o) {
+                                return o.end;
+                            }
+                        })
+                        .compare();
+            }
+        });
+        System.out.println(items);
+    }
+
+    @Test(priority = 7)
+    public void testAscCompares() {
+        {
+            int result = CompareTools.stream()
+                    .asc(2, 2)
+                    .asc("3", "4")
+                    .compare();
+            Assert.assertEquals(result, -1);
+        }
+        {
+            int result = CompareTools.stream()
+                    .asc(2, 2)
+                    .desc("3", "4")
+                    .asc(4, 4)
+                    .compare();
+            Assert.assertEquals(result, 1);
+        }
+        {
+            int result = CompareTools.stream()
+                    .asc(2, 2)
+                    .asc("3", "3")
+                    .asc(4, 4)
+                    .compare();
+            Assert.assertEquals(result, 0);
+        }
+    }
+
+    private static class Item1 implements Comparable<Item1> {
+        private final Integer start;
+        private final Integer end;
+
+        public Item1(Integer start, Integer end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        public Integer getStart() {
+            return start;
+        }
+
+        public Integer getEnd() {
+            return end;
+        }
+
+        @Override
+        public int compareTo(Item1 o) {
+            if (o == null) {
+                return -1;
+            }
+            return CompareTools.stream()
+                    .asc(start, o.getStart())
+                    .asc(end, o.getEnd())
+                    .compare();
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder buffer = new StringBuilder();
+            buffer.append(start).append('-').append(end);
+            return buffer.toString();
+        }
+    }
+
+    private static class Item2 extends Item1 {
+
+        public Item2(Integer start, Integer end) {
+            super(start, end);
+        }
+    }
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/compare/SortTest.java b/tools/src/test/java/com/gitee/qdbp/tools/compare/SortTest.java
index f517a96c9660d346bc3b587a9b9792ca56d72f66..68c10e23e56f8bb8685d111274b8286869658d22 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/compare/SortTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/compare/SortTest.java
@@ -4,7 +4,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
 import com.gitee.qdbp.able.instance.ComplexComparator;
 import com.gitee.qdbp.able.instance.MapFieldComparator;
 import com.gitee.qdbp.able.jdbc.ordering.Orderings;
@@ -17,18 +16,18 @@ public class SortTest {
     public static void main(String[] args) {
 
         List<Map<String, Object>> list = new ArrayList<>();
-        list.add(JSON.parseObject("{id:6,name:'kkk',dept:'BB'}"));
+        list.add(JsonTools.parseAsMap("{id:6,name:'kkk',dept:'BB'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-09-09"));
-        list.add(JSON.parseObject("{id:1,name:'bbb',dept:'AA'}"));
+        list.add(JsonTools.parseAsMap("{id:1,name:'bbb',dept:'AA'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-08-08"));
-        list.add(JSON.parseObject("{id:3,name:'hhh',dept:'AA'}"));
+        list.add(JsonTools.parseAsMap("{id:3,name:'hhh',dept:'AA'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-11-11"));
-        list.add(JSON.parseObject("{id:5,name:'eee',dept:'CC'}"));
+        list.add(JsonTools.parseAsMap("{id:5,name:'eee',dept:'CC'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-10-10"));
-        list.add(JSON.parseObject("{id:4,name:'ddd',dept:'BB'}"));
+        list.add(JsonTools.parseAsMap("{id:4,name:'ddd',dept:'BB'}"));
         list.get(list.size() - 1).put("birthday", DateTools.parse("1988-08-08"));
-        list.add(JSON.parseObject("{id:7,name:'fff'}"));
-        list.add(JSON.parseObject("{id:2,name:'aaa'}"));
+        list.add(JsonTools.parseAsMap("{id:7,name:'fff'}"));
+        list.add(JsonTools.parseAsMap("{id:2,name:'aaa'}"));
         System.out.println(JsonTools.toJsonString(list));
 
         {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/crypto/AesCipherTest.java b/tools/src/test/java/com/gitee/qdbp/tools/crypto/AesCipherTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff2fe98b16a108fc428f796582baca75604d9a3e
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/crypto/AesCipherTest.java
@@ -0,0 +1,235 @@
+package com.gitee.qdbp.tools.crypto;
+
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.codec.bytes.Base58Codec;
+import com.gitee.qdbp.tools.codec.bytes.Base64Codec;
+import com.gitee.qdbp.tools.utils.RandomTools;
+
+/**
+ * AesCipherTest
+ *
+ * @author zhaohuihua
+ * @version 20230308
+ */
+@Test
+public class AesCipherTest {
+
+    @Test
+    public void test() {
+        System.out.println("-- AES CBC ---------------------------");
+        String ciphertext="nohc1YmDPtMGG3DtxkeoJiUb+OuCOq8UAahU8VZ5WrVGC2u7kxamfTjpeN1KeQ2NHuAvAozSoOWzwy+Yy4id0h2dnST+HGTiyWCwpo+ZSfhvKZHr+3vr5TvFayq4ve5uXEUKjW03OALMJTnB7b59V5SGNcGX9K420Ms7bVR+9zDkgT3GBAeCxUpS2vcvSdk1DYwaTD/wOKUN+zCNMsXGNqK4bLkxRzVbLL5nr/CZxvoLExbnTAKQq62aSxazLFRtfP5iB57soCA2ihCjQGSKDpZwGawO4HF78jptW7PzRl2oIDtN4z1vKfy/owVeFZERe60fWHHTCm56ZLbEiU/HShTP+hPrUc3ktZVXZXNZVdQ8Np9wify5l6sR/M4ew4LmGVr1GZldQ0ohK1LAc/2oZbfG0P/YVCI8oaW38RCJMfWpqHKw8nNnjqUsbn1QLLOm6DpA8Gwq/3GG23lSqazG0g==";
+        String secretKey="DZO3/fF/kztY43IIhHOwxw==";
+        String initVector="dd8GXfeZMzOr1U9CL+R80w==";
+        System.out.println("ciphertext = " + ciphertext);
+        System.out.println("secretKey  = " + secretKey);
+        System.out.println("initVector = " + initVector);
+        String plaintext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(secretKey, Base64Codec.INSTANCE)
+                    .byteCodec(Base64Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector, Base64Codec.INSTANCE);
+            // 解密测试
+            plaintext = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + plaintext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(secretKey, Base64Codec.INSTANCE)
+                    .byteCodec(Base64Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector, Base64Codec.INSTANCE);
+            // 加密测试
+            String output = aesCipher.encrypt(plaintext);
+            System.out.println("加密: " + output);
+        }
+    }
+
+    @Test
+    public void testEcbCipher1() {
+        System.out.println("-- AES ECB 1 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        // 生成随机AES密码
+        String aesKey = RandomTools.generateString(20);
+        System.out.println("密钥: " + aesKey);
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildEcbCipher();
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildEcbCipher();
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+    @Test
+    public void testEcbCipher2() {
+        System.out.println("-- AES ECB 2 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        byte[] keyBytes = new byte[32];
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(keyBytes)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildEcbCipher();
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(keyBytes)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildEcbCipher();
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+
+    @Test
+    public void testCbcCipher1() {
+        System.out.println("-- AES CBC 1 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        // 生成随机AES密码
+        String aesKey = RandomTools.generateString(20);
+        System.out.println("密钥: " + aesKey);
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher();
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher();
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+    @Test
+    public void testCbcCipher2() {
+        System.out.println("-- AES CBC 2 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        byte[] keyBytes = new byte[32];
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(keyBytes)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher();
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(keyBytes)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher();
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+    @Test
+    public void testCbcCipher3() {
+        System.out.println("-- AES CBC 3 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        // 生成随机AES密码
+        String aesKey = RandomTools.generateString(20);
+        String initVector = RandomTools.generateString(16);
+        System.out.println("密钥: " + aesKey);
+        System.out.println("向量: " + initVector);
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector);
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector);
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+
+    @Test
+    public void testCbcCipher4() {
+        System.out.println("-- AES CBC 4 ---------------------------");
+        String string = "[AES 加密解密 测试]";
+        // 生成随机AES密码
+        byte[] aesKey = AesCipher.generateSecretKey();
+        byte[] initVector = AesCipher.generateCbcInitVector();
+        System.out.println("密钥: " + Base58Codec.INSTANCE.encode(aesKey));
+        System.out.println("向量: " + Base58Codec.INSTANCE.encode(initVector));
+        String ciphertext;
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector);
+            // 加密测试
+            ciphertext = aesCipher.encrypt(string);
+            System.out.println("加密: " + ciphertext);
+        }
+        {
+            // AES实例
+            CipherService aesCipher = AesCipher.builder()
+                    .secretKey(aesKey)
+                    .byteCodec(Base58Codec.INSTANCE)
+                    .buildCbcCipher()
+                    .initVector(initVector);
+            // 解密测试
+            String output = aesCipher.decrypt(ciphertext);
+            System.out.println("解密: " + output);
+        }
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/crypto/RsaTest.java b/tools/src/test/java/com/gitee/qdbp/tools/crypto/RsaTest.java
index 3d74168df9874678c20f2afa6a4cc7af590e6ea3..f5382842692ca907911b4471f9c1c9fbb687ae01 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/crypto/RsaTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/crypto/RsaTest.java
@@ -10,8 +10,9 @@ public class RsaTest {
     }
 
     protected static void testCipher(String string) {
-        RsaCipher rsaCipher = new RsaCipher(512, Base58Codec.INSTANCE);
-        System.out.println("公钥: " + rsaCipher.getPublicKey());
+        RsaCipher rsaCipher = RsaCipher.builder()
+                .byteCodec(Base58Codec.INSTANCE).generateKey(512).build();
+        System.out.println("公钥: " + rsaCipher.options().getPublicKey());
         String ciphertext = rsaCipher.encrypt(string);
         System.out.println("加密: " + ciphertext);
         // A用B的公钥+自己的私钥解密
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/EmployeeInfo.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/EmployeeInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..30d19c6637afea512640fc5986341fa4b201bc73
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/EmployeeInfo.java
@@ -0,0 +1,79 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.util.Date;
+
+class EmployeeInfo {
+
+    private Long id;
+    private String dept;
+    private String name;
+    private Boolean positive;
+    private Integer height;
+    private Date birthday;
+    private Gender gender;
+    private Integer subsidy;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getDept() {
+        return dept;
+    }
+
+    public void setDept(String dept) {
+        this.dept = dept;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Boolean getPositive() {
+        return positive;
+    }
+
+    public void setPositive(Boolean positive) {
+        this.positive = positive;
+    }
+
+    public Integer getHeight() {
+        return height;
+    }
+
+    public void setHeight(Integer height) {
+        this.height = height;
+    }
+
+    public Date getBirthday() {
+        return birthday;
+    }
+
+    public void setBirthday(Date birthday) {
+        this.birthday = birthday;
+    }
+
+    public Gender getGender() {
+        return gender;
+    }
+
+    public void setGender(Gender gender) {
+        this.gender = gender;
+    }
+
+    public Integer getSubsidy() {
+        return subsidy;
+    }
+
+    public void setSubsidy(Integer subsidy) {
+        this.subsidy = subsidy;
+    }
+}
\ No newline at end of file
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
index d4b0ca644b956b20b57730ddfcaad35388ab19f0..5ce00da2f49de665df2518267c19331e6b7f764a 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.java
@@ -11,9 +11,6 @@ import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONException;
-import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.tools.excel.model.RowInfo;
 import com.gitee.qdbp.tools.excel.utils.MetadataTools;
@@ -39,21 +36,19 @@ public class ExcelInOutTest {
 
         @Override
         public void callback(Map<String, Object> map, RowInfo row) throws ServiceException {
-            JSONObject json = new JSONObject();
-            json.putAll(map);
             EmployeeInfo model;
             try {
-                model = JSON.toJavaObject(json, EmployeeInfo.class);
+                model = JsonTools.mapToBean(map, EmployeeInfo.class);
                 employees.add(model);
-            } catch (JSONException e) {
+            } catch (Exception e) {
                 throw new ServiceException(ExcelErrorCode.EXCEL_DATA_FORMAT_ERROR, e);
             }
             System.out.println(index + "\timport: " + JsonTools.toLogString(model));
         }
     }
 
-    public static void main(String[] args) {
-        URL path = PathTools.findClassResource(ExcelInOutTest.class, "ExcelInOutTest.txt");
+    public static void main(String[] args) throws Exception {
+        URL path = PathTools.findClassResource(ExcelInOutTest.class, "ExcelInOutTest.properties");
         System.out.println(path);
         Properties original = PropertyTools.load(path, "UTF-8");
         Properties common = PropertyTools.filter(original, "common."); // 公共配置
@@ -63,7 +58,7 @@ public class ExcelInOutTest {
         test(PropertyTools.concat(new Properties(), common, test2), 2);
     }
 
-    private static void test(Properties properties, int index) {
+    private static void test(Properties properties, int index) throws Exception {
 
         URL xlsx = PathTools.findClassResource(ExcelInOutTest.class, "员工信息导入." + index + ".xlsx");
         XMetadata metadata = MetadataTools.parseProperties(properties);
@@ -82,9 +77,6 @@ public class ExcelInOutTest {
             if (succ != 8 || fail != 2) {
                 throw new Exception("不符合预期结果: index=" + index + ", succ!=8, fail!=2");
             }
-        } catch (Exception e) {
-            e.printStackTrace();
-            return;
         }
 
         URL tpl = PathTools.findClassResource(ExcelInOutTest.class, "员工信息(模板)." + index + ".xlsx");
@@ -135,85 +127,4 @@ public class ExcelInOutTest {
             System.out.println(index + "\t" + this.getName() + " done --> " + ConvertTools.toDuration(start, true));
         }
     }
-
-    public enum Gender {
-        UNKNOWN, MALE, FEMALE
-    }
-
-    public static class EmployeeInfo {
-
-        private Long id;
-        private String dept;
-        private String name;
-        private Boolean positive;
-        private Integer height;
-        private Date birthday;
-        private Gender gender;
-        private Integer subsidy;
-
-        public Long getId() {
-            return id;
-        }
-
-        public void setId(Long id) {
-            this.id = id;
-        }
-
-        public String getDept() {
-            return dept;
-        }
-
-        public void setDept(String dept) {
-            this.dept = dept;
-        }
-
-        public String getName() {
-            return name;
-        }
-
-        public void setName(String name) {
-            this.name = name;
-        }
-
-        public Boolean getPositive() {
-            return positive;
-        }
-
-        public void setPositive(Boolean positive) {
-            this.positive = positive;
-        }
-
-        public Integer getHeight() {
-            return height;
-        }
-
-        public void setHeight(Integer height) {
-            this.height = height;
-        }
-
-        public Date getBirthday() {
-            return birthday;
-        }
-
-        public void setBirthday(Date birthday) {
-            this.birthday = birthday;
-        }
-
-        public Gender getGender() {
-            return gender;
-        }
-
-        public void setGender(Gender gender) {
-            this.gender = gender;
-        }
-
-        public Integer getSubsidy() {
-            return subsidy;
-        }
-
-        public void setSubsidy(Integer subsidy) {
-            this.subsidy = subsidy;
-        }
-
-    }
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.txt b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.properties
similarity index 100%
rename from tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.txt
rename to tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelInOutTest.properties
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelMapTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelMapTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..87ebd51aba19d022b07127114e62e28230e21974
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelMapTest.java
@@ -0,0 +1,52 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.io.File;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import com.gitee.qdbp.tools.excel.model.FailedInfo;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
+
+public class ExcelMapTest {
+
+    public static void main(String[] args) throws Exception {
+        URL sourceExcel = PathTools.findClassResource(ExcelMapTest.class, "员工信息导入.1.xlsx");
+        System.out.println("sourceExcel = " + PathTools.toUriPath(sourceExcel));
+
+        XMetadata metadata = XMetadata.of(4, "*id|*name|positive|height||birthday|gender|subsidy");
+        XExcelParser parser = new XExcelParser(metadata);
+        ImportResult<Map<String, Object>> result = parser.parseAsMap(sourceExcel);
+
+        int fail = result.getFailed().size();
+        int succ = result.getTotal() - fail;
+        System.out.printf("finished: succ=%s, fail=%s%n", succ, fail);
+        System.out.println("---------------");
+        System.out.println("fail: " + fail);
+        for (FailedInfo item : result.getFailed()) {
+            System.out.println(JsonTools.toLogString(item));
+        }
+        List<Map<String, Object>> employees = result.getContents();
+        System.out.println("---------------");
+        System.out.println("succ: " + succ);
+        for (Map<String, Object> item : employees) {
+            System.out.println(JsonTools.toLogString(item));
+        }
+        System.out.println("---------------");
+
+        URL templatePath = PathTools.findClassResource(ExcelMapTest.class, "员工信息(模板).1.xlsx");
+        String saveFolder = PathTools.getOutputFolder(sourceExcel, "./");
+        String targetExcel = PathTools.concat(saveFolder, "员工信息导出.1.xlsx");
+        System.out.println("templatePath = " + PathTools.toUriPath(templatePath));
+        System.out.println("targetExcel = " + targetExcel);
+
+        // 导出
+        XExcelExporter exporter = new XExcelExporter(metadata);
+        Date start = new Date();
+        exporter.export(employees, templatePath, new File(targetExcel));
+
+        System.out.println("export successful, " + ConvertTools.toDuration(start));
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSheetTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSheetTest.java
index 7bc01a7f4123471127524f9d3a75ff2c7b86338f..2e5648ce1dd2603293f8602e86d4189431114581 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSheetTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSheetTest.java
@@ -1,15 +1,17 @@
 package com.gitee.qdbp.tools.excel;
 
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
-import com.gitee.qdbp.tools.excel.utils.ExcelTools;
-import com.gitee.qdbp.tools.files.PathTools;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import com.gitee.qdbp.tools.excel.utils.ExcelTools;
+import com.gitee.qdbp.tools.files.PathTools;
 
 /**
  * Sheet页复制(前景色和背景色复制有问题)
@@ -19,19 +21,19 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook;
  */
 public class ExcelSheetTest {
 
-    public static void main(String[] args) {
+    public static void main(String[] args) throws IOException, InvalidFormatException {
         String outFile = "D:/ExcelSheet.xlsx";
 
         URL xlsx = PathTools.findClassResource(ExcelInOutTest.class, "员工信息导入.1.xlsx");
 
-        try (InputStream is = xlsx.openStream(); OutputStream os = new FileOutputStream(outFile);
-                Workbook src = WorkbookFactory.create(is); Workbook wb = new XSSFWorkbook()) {
+        try (InputStream is = xlsx.openStream();
+                OutputStream os = new FileOutputStream(outFile);
+                Workbook src = WorkbookFactory.create(is);
+                Workbook wb = new XSSFWorkbook()) {
             Sheet srcSheet = src.getSheetAt(0);
             Sheet targetSheet = wb.createSheet(srcSheet.getSheetName());
             ExcelTools.copySheet(srcSheet, targetSheet, true);
             wb.write(os);
-        } catch (Exception e) {
-            e.printStackTrace();
         }
 
     }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ca7033a8690e82a076c4ee626960921199359b2
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.java
@@ -0,0 +1,57 @@
+package com.gitee.qdbp.tools.excel;
+
+import java.io.File;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+import com.gitee.qdbp.tools.excel.model.FailedInfo;
+import com.gitee.qdbp.tools.excel.utils.MetadataTools;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.ConvertTools;
+import com.gitee.qdbp.tools.utils.JsonTools;
+
+public class ExcelSimpleTest {
+
+    public static void main(String[] args) throws Exception {
+        URL configPath = PathTools.findClassResource(ExcelSimpleTest.class, "ExcelSimpleTest.properties");
+        URL sourceExcel = PathTools.findClassResource(ExcelSimpleTest.class, "员工信息导入.1.xlsx");
+        System.out.println("configPath = " + PathTools.toUriPath(configPath));
+        System.out.println("sourceExcel = " + PathTools.toUriPath(sourceExcel));
+
+        XMetadata metadata = MetadataTools.parseProperties(configPath);
+        XExcelParser parser = new XExcelParser(metadata);
+        ImportResult<EmployeeInfo> result = parser.parseAsBean(sourceExcel, EmployeeInfo.class);
+
+        int fail = result.getFailed().size();
+        int succ = result.getTotal() - fail;
+        System.out.printf("finished: succ=%s, fail=%s%n", succ, fail);
+        if (succ != 8 || fail != 2) {
+            throw new Exception("不符合预期结果: succ!=8, fail!=2");
+        }
+        System.out.println("---------------");
+        System.out.println("fail: " + fail);
+        for (FailedInfo item : result.getFailed()) {
+            System.out.println(JsonTools.toLogString(item));
+        }
+        List<EmployeeInfo> employees = result.getContents();
+        System.out.println("---------------");
+        System.out.println("succ: " + succ);
+        for (EmployeeInfo item : employees) {
+            System.out.println(JsonTools.toLogString(item));
+        }
+        System.out.println("---------------");
+
+        URL templatePath = PathTools.findClassResource(ExcelSimpleTest.class, "员工信息(模板).1.xlsx");
+        String saveFolder = PathTools.getOutputFolder(sourceExcel, "./");
+        String targetExcel = PathTools.concat(saveFolder, "员工信息导出.1.xlsx");
+        System.out.println("templatePath = " + PathTools.toUriPath(templatePath));
+        System.out.println("targetExcel = " + targetExcel);
+
+        // 导出
+        XExcelExporter exporter = new XExcelExporter(metadata);
+        Date start = new Date();
+        exporter.export(employees, templatePath, new File(targetExcel));
+
+        System.out.println("export successful, " + ConvertTools.toDuration(start));
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0e2b585a2c5c8aa581ab2ca596a42187b91a6be1
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/ExcelSimpleTest.properties
@@ -0,0 +1,41 @@
+## 字段列表
+columns = *id|*name|positive|height||birthday|gender|subsidy
+
+## 跳过的行数(不配置时默认取字段行或标题行的下一行)
+# skip.rows = 2
+## 标题行号,从1开始
+header.rows = 2-4
+## 页脚行号,从1开始,只对导出生效
+## 导入时每个excel的页脚位置有可能不一样, 所以导入不能指定页脚行, 只能在导入之前把页脚删掉
+## 如果页脚有公式, 那么数据行必须至少2行, 公式的范围必须包含这两行数据, 如SUM($E$5:E6), 而不能是SUM($E$5:E5), 否则公式不会计算
+footer.rows = 7-9
+## 包含指定关键字时跳过此行
+## A列为空, 或B列包含小计且H列包含元, 或B列包含总计且H列包含元
+# skip.row.when = { A:"NULL" }, { B:"小计", H:"元" }, { B:"总计", H:"元" }
+
+
+## 按索引号配置Sheet页
+## 配置规则: * 表示全部
+## 配置规则: 1|2|5-8|12
+## 配置规则: !1 表示排除第1个
+## 配置规则: !1|3|5 表示排除第1/3/5个
+# sheet.index = !1
+## 按索名称配置Sheet页(sheet1这种未命名的sheet是默认排除的)
+## 配置规则: * 表示全部
+## 配置规则: 开发|测试
+## 配置规则: !说明|描述 表示排除
+sheet.name = !说明
+## 页签名称填充至哪个字段
+sheet.name.fill.to = dept
+## 行序号填充至哪个字段
+row.index.fill.to = index
+
+## 转换规则
+rules.birthday = { date: "yyyy/MM/dd" }
+rules.positive = { map: { true:"已转正|是|Y", false:"未转正|否|N" } }
+# 多个规则
+rules.height = { number:"int" }, { ignoreIllegalValue:true }
+# rules.gender = { map:{ UNKNOWN:"未知|0", MALE:"男|1", FEMALE:"女|2" } }, { ignoreIllegalValue:true }
+# rules.gender = { map:"UNKNOWN:未知|0, MALE:男|1, FEMALE:女|2" }, { ignoreIllegalValue:true }
+rules.gender = { map:"UNKNOWN:未知|0, MALE:男|1, FEMALE:女|2" }
+
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/Gender.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/Gender.java
new file mode 100644
index 0000000000000000000000000000000000000000..37f88374f975e05826ada25ed0933e99d9c72754
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/Gender.java
@@ -0,0 +1,5 @@
+package com.gitee.qdbp.tools.excel;
+
+enum Gender {
+    UNKNOWN, MALE, FEMALE
+}
\ No newline at end of file
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/excel/beans/ExcelBeansTest.java b/tools/src/test/java/com/gitee/qdbp/tools/excel/beans/ExcelBeansTest.java
index ee30eb984402fdaa67ea8d43c0e1b9ff711eef6a..336ec58c0e6dd72ce30e0abf43665674825166bd 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/excel/beans/ExcelBeansTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/excel/beans/ExcelBeansTest.java
@@ -1,5 +1,6 @@
 package com.gitee.qdbp.tools.excel.beans;
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
@@ -9,8 +10,6 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import org.apache.poi.POIXMLException;
-import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.ss.usermodel.Workbook;
 import org.apache.poi.ss.usermodel.WorkbookFactory;
 import org.slf4j.Logger;
@@ -66,15 +65,20 @@ public class ExcelBeansTest {
             compareHolidays(config);
             // 比较还本计划
             comparePrincipalSchedules(container, firstRepayPrincipalDate);
+        } catch (FileNotFoundException e) {
+            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
         } catch (IOException e) {
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_READ_ERROR);
-        } catch (POIXMLException e) {
+            throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+        } catch (Exception e) {
+            // poi-ooxml-3.17: org.apache.poi.POIXMLException
+            // poi-ooxml-4.1.2: org.apache.poi.ooxml.POIXMLException
             log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
-        } catch (InvalidFormatException e) {
-            log.error("read excel error.", e);
-            throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR);
+            if ("InvalidFormatException".equals(e.getClass().getSimpleName())) {
+                throw new ServiceException(FileErrorCode.FILE_FORMAT_ERROR, e);
+            } else {
+                throw new ServiceException(FileErrorCode.FILE_READ_ERROR, e);
+            }
         }
     }
 
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
index 4a7e8c1222b13af6f4efbb84b578d1a616460030..33bedb13106fb9fd6e25c9bff321facb359f939f 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpExecutorTest.java
@@ -1,22 +1,18 @@
 package com.gitee.qdbp.tools.http;
 
 import java.net.URL;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import com.gitee.qdbp.able.result.ResultCode;
 import com.gitee.qdbp.tools.codec.DigestTools;
-import com.gitee.qdbp.tools.codec.HexTools;
 import com.gitee.qdbp.tools.files.PathTools;
 import com.gitee.qdbp.tools.http.HostUrlConfig.KeyedHttpUrl;
 import com.gitee.qdbp.tools.http.HttpTools.HttpJsonImpl;
 import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
 import com.gitee.qdbp.tools.utils.RandomTools;
 
 public class HttpExecutorTest extends HttpExecutor {
@@ -56,7 +52,7 @@ public class HttpExecutorTest extends HttpExecutor {
             String versionSecretKey = config.getString("cim.interface.version.secret.key");
 
             String createTime = new SimpleDateFormat(createTimePattern).format(new Date());
-            long ts = Long.parseLong(RandomTools.generateNumber(5));
+            long ts = RandomTools.generateNumber(10001, 99999);
             String info = account + ts + imeiuuid + appcode + versionSecretKey;
 
             map.put("appCode", appcode);
@@ -66,36 +62,30 @@ public class HttpExecutorTest extends HttpExecutor {
             map.put("sourceType", config.getString("cim.interface.source.type"));
             map.put("createTime", createTime);
             map.put("ts", ts);
+            map.put("digest", DigestTools.md5(info));
             if (data != null) {
                 map.put("jsonData", data);
             }
 
-            try {
-                byte[] digests = MessageDigest.getInstance("MD5").digest(info.getBytes());
-                map.put("digest", HexTools.toString(digests));
-            } catch (NoSuchAlgorithmException e) {
-                throw new RuntimeException(e);
-            }
-
             return map;
         }
 
         @Override
         public ResponseMessage parseResult(HttpUrl hurl, String string) throws Exception {
             // { resultCode:int, resultJson:{ msgCode:string, message:string, data:json } }
-            JSONObject json = JSON.parseObject(string);
+            Map<String, Object> json = JsonTools.parseAsMap(string);
             ResponseMessage result = new ResponseMessage();
-            String resultCode = json.getString("resultCode");
-            if (!resultCode.equals(config.getString("cim.interface.success"))) {
+            String resultCode = MapTools.getString(json, "resultCode");
+            if (resultCode == null || !resultCode.equals(config.getString("cim.interface.success"))) {
                 // 这一类异常不应该抛上去给用户看到了, 直接返回接口调用失败, 日志HttpTools会统一记录的
                 ResultCode code = ResultCode.REMOTE_SERVICE_FAIL;
                 throw new RemoteServiceException(code.getCode(), code.getMessage());
             } else {
-                String resultString = json.getString("resultJson");
-                JSONObject resultJson = JSON.parseObject(resultString);
-                result.setCode(resultJson.getString("msgCode"));
-                result.setMessage(resultJson.getString("message"));
-                result.setBody(resultJson.getString("data"));
+                String resultString = MapTools.getString(json, "resultJson");
+                Map<String, Object> resultJson = JsonTools.parseAsMap(resultString);
+                result.setCode(MapTools.getString(resultJson, "msgCode"));
+                result.setMessage(MapTools.getString(resultJson, "message"));
+                result.setBody(MapTools.getString(resultJson, "data"));
 
                 String key = ((KeyedHttpUrl) hurl).getKey();
                 if (result.getCode().equals(config.getString(key + ".success"))) {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpToolsTest.java
index a99a7d197f727b4bce9ed9c727c0d483c4c5c1c9..4f51830dfd999f464f69c3fa16e130d8b175587e 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/http/HttpToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/http/HttpToolsTest.java
@@ -1,7 +1,7 @@
 package com.gitee.qdbp.tools.http;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
+import java.util.Map;
+import com.gitee.qdbp.tools.utils.JsonTools;
 
 
 public class HttpToolsTest {
@@ -10,7 +10,7 @@ public class HttpToolsTest {
         String url = "http://cimqas.cttq.com/cim-clinic-gwy/operate/clinic/online/doc/cancel/record";
         String text =
                 "{\"createTime\":\"2016-05-10\",\"sourceType\":\"pc\",\"ts\":34370,\"appCode\":\"A000\",\"accountType\":1,\"account\":\"13913001382\",\"digest\":\"25AE1ED2CDF19CD5A36CE58E1B2158CE\",\"imeiuuid\":\"359596063773059\",\"jsonData\":{\"causeContext\":\"诊室异常\",\"bussiType\":5,\"clinicDate\":\"2016-05-10 09:12:00\",\"clinicCid\":\"20160510085941749062992ee3a8ec60\"}}";
-        JSONObject json = JSON.parseObject(text);
+        Map<String, Object> json = JsonTools.parseAsMap(text);
         try {
             System.out.println(HttpTools.json.post(url, json));
         } catch (HttpException e) {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java b/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
index d8e4c200ccb43941ca73a241518881d82570cebe..7a236c6af67b12f887e24599d7dad401c7242b22 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/http/XxxAuthHttpExecutor.java
@@ -1,19 +1,17 @@
 package com.gitee.qdbp.tools.http;
 
 import java.net.URL;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
 import com.gitee.qdbp.able.exception.ServiceException;
 import com.gitee.qdbp.able.result.ResponseMessage;
 import com.gitee.qdbp.able.result.ResultCode;
-import com.gitee.qdbp.tools.codec.HexTools;
+import com.gitee.qdbp.tools.codec.DigestTools;
 import com.gitee.qdbp.tools.files.PathTools;
 import com.gitee.qdbp.tools.http.HttpTools.HttpJsonImpl;
+import com.gitee.qdbp.tools.utils.JsonTools;
+import com.gitee.qdbp.tools.utils.MapTools;
 import com.gitee.qdbp.tools.utils.RandomTools;
 
 /**
@@ -60,7 +58,7 @@ public class XxxAuthHttpExecutor extends HttpExecutor {
 
     // 如果针对每个接口存在不一样的请求参数或响应报文配置, 可将hurl强转为KeyedHttpUrl
     // String key = ((KeyedHttpUrl) hurl).getKey();
-    // config.getString(key + ".any.subffix"); // 针对每个接口作不同的配置
+    // config.getString(key + ".any.suffix"); // 针对每个接口作不同的配置
 
     private static class XxxAuthHandler extends ConfigHttpHandler {
 
@@ -73,7 +71,7 @@ public class XxxAuthHttpExecutor extends HttpExecutor {
             // 组装基础参数
             String account = config.getString("xxx.auth.account"); // 本平台在xxx平台申请的账号
             String password = config.getString("xxx.auth.password"); // 对应的密码
-            long ts = Long.parseLong(RandomTools.generateNumber(5)); // xxx平台要求用一个5位随机数加密
+            long ts = RandomTools.generateNumber(10001, 99999); // xxx平台要求用一个5位随机数加密
             long time = System.currentTimeMillis();
 
             map.put("account", account);
@@ -83,13 +81,9 @@ public class XxxAuthHttpExecutor extends HttpExecutor {
                 map.put("body", data);
             }
 
-            try { // 计算摘要
-                String info = account + password + ts + time;
-                byte[] digests = MessageDigest.getInstance("MD5").digest(info.getBytes());
-                map.put("digest", HexTools.toString(digests));
-            } catch (NoSuchAlgorithmException e) { // 这个异常不会出现, 除非你用的JDK不支持MD5加密
-                throw new IllegalStateException("NoSuchAlgorithm: MD5", e);
-            }
+            // 计算摘要
+            String info = account + password + ts + time;
+            map.put("digest", DigestTools.md5(info));
 
             return map;
         }
@@ -99,23 +93,23 @@ public class XxxAuthHttpExecutor extends HttpExecutor {
             // xxx平台的响应报文是一个两层结构
             // { code:int, json:{ resultCode:string, resultMessage:string, resultData:json } }
             // code是系统异常, 0=成功; returnCode是业务异常, 000000=成功
-            JSONObject json = JSON.parseObject(string);
-            String sysCode = json.getString("code");
-            if (!sysCode.equals(config.getString("xxx.auth.code.sys.success"))) {
+            Map<String, Object> json = JsonTools.parseAsMap(string);
+            String sysCode = MapTools.getString(json, "code");
+            if (sysCode == null || !sysCode.equals(config.getString("xxx.auth.code.sys.success"))) {
                 // xxx.auth.code.sys.success = 0, 不等于0的都是失败
                 // 这一类异常不应该抛上去给用户看到了, 直接返回接口调用失败, 日志HttpTools会统一记录的
                 ResultCode resultCode = ResultCode.REMOTE_SERVICE_FAIL;
                 throw new RemoteServiceException(resultCode.getCode(), resultCode.getMessage());
             } else {
-                String jsonString = json.getString("json");
-                JSONObject jsonObject = JSON.parseObject(jsonString);
-                String bizCode = jsonObject.getString("resultCode");
-                if (!bizCode.equals(config.getString("xxx.auth.code.biz.success"))) {
+                String jsonString = MapTools.getString(json, "json");
+                Map<String, Object> jsonObject =JsonTools.parseAsMap(jsonString);
+                String bizCode = MapTools.getString(jsonObject, "resultCode");
+                if (bizCode == null || !bizCode.equals(config.getString("xxx.auth.code.biz.success"))) {
                     // xxx.auth.code.biz.success = 000000, 不等于000000的都是失败
-                    throw new RemoteServiceException(sysCode, jsonObject.getString("resultMessage"));
+                    throw new RemoteServiceException(sysCode, MapTools.getString(jsonObject, "resultMessage"));
                 } else {
                     ResponseMessage result = new ResponseMessage();
-                    result.setBody(jsonObject.getString("resultData"));
+                    result.setBody(MapTools.getString(jsonObject, "resultData"));
                     return result;
                 }
             }
@@ -143,7 +137,7 @@ class UserAuthService {
 
     public UserInfo queryUserInfo(UserQueryParams contidion) throws ServiceException {
         try {
-            JSONObject params = (JSONObject) JSON.toJSON(contidion);
+            Map<String, Object> params = JsonTools.beanToMap(contidion);
             return XxxAuthHttpExecutor.me.query("xxx.auth.user.baseinfo", params, UserInfo.class);
         } catch (RemoteServiceException e) {
             throw new ServiceException(e, e); // ServiceException(IResultMessage, Throwable)
@@ -154,7 +148,7 @@ class UserAuthService {
 
     public List<UserInfo> listUserInfo(UserQueryParams contidion) throws ServiceException {
         try {
-            JSONObject params = (JSONObject) JSON.toJSON(contidion);
+            Map<String, Object> params = JsonTools.beanToMap(contidion);
             return XxxAuthHttpExecutor.me.list("xxx.auth.user.list", params, UserInfo.class);
         } catch (RemoteServiceException e) {
             throw new ServiceException(e, e); // ServiceException(IResultMessage, Throwable)
@@ -166,11 +160,11 @@ class UserAuthService {
     /** 注册并返回用户ID **/
     public String register(UserInfo userInfo) throws ServiceException {
         try {
-            JSONObject params = (JSONObject) JSON.toJSON(userInfo);
+            Map<String, Object> params = JsonTools.beanToMap(userInfo);
             // 注册接口返回的resultData = { userId:string }
             ResponseMessage resp = XxxAuthHttpExecutor.me.execute("xxx.auth.user.register", params);
-            JSONObject result = JSON.parseObject((String) resp.getBody());
-            return result.getString("userId");
+            Map<String, Object> result = JsonTools.parseAsMap((String) resp.getBody());
+            return MapTools.getString(result, "userId");
         } catch (RemoteServiceException e) {
             throw new ServiceException(e, e); // ServiceException(IResultMessage, Throwable)
         } catch (Exception e) {
@@ -185,8 +179,8 @@ class UserAuthService {
             params.put("userId", userId);
             // 余额接口返回的resultData = { balance:double }
             ResponseMessage resp = XxxAuthHttpExecutor.me.execute("xxx.auth.user.balance", params);
-            JSONObject result = JSON.parseObject((String) resp.getBody());
-            return result.getDouble("balance");
+            Map<String, Object> result = JsonTools.parseAsMap((String) resp.getBody());
+            return MapTools.getValue(result, "balance", null, Double.class);
         } catch (RemoteServiceException e) {
             throw new ServiceException(e, e); // ServiceException(IResultMessage, Throwable)
         } catch (Exception e) {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/specialized/PinyinChecker.java b/tools/src/test/java/com/gitee/qdbp/tools/specialized/PinyinChecker.java
index 28b88ae29642275c7ebbe363e7124a1bc904bece..55a563df9fc8f8d7a0a74b69a9d6c06201c48991 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/specialized/PinyinChecker.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/specialized/PinyinChecker.java
@@ -13,6 +13,8 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.gitee.qdbp.tools.files.FileTools;
 import com.gitee.qdbp.tools.files.PathTools;
 import com.gitee.qdbp.tools.http.BaseHttpHandler;
@@ -23,8 +25,6 @@ import com.gitee.qdbp.tools.utils.StringTools;
 import com.gitee.qdbp.tools.utils.VerifyTools;
 import com.github.stuxuhai.jpinyin.PinyinFormat;
 import com.github.stuxuhai.jpinyin.PinyinHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * 拼音正确性检查
@@ -36,8 +36,6 @@ public class PinyinChecker {
 
     private static final Logger log = LoggerFactory.getLogger(PinyinChecker.class);
 
-    private static final Pattern SEPARATOR = Pattern.compile("/");
-
     public static void main(String[] args) throws IOException {
         PinyinChecker checker = new PinyinChecker();
         // System.out.println(checker.findPinyinByBaiduHanyu("觉"));
@@ -68,7 +66,7 @@ public class PinyinChecker {
                     writeLine(writer, line);
                     continue;
                 }
-                String[] items = SEPARATOR.split(line);
+                String[] items = StringTools.split(line, '/');
                 if (items.length < 3) {
                     writeLine(writer, line);
                     continue;
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/CartesianTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/CartesianTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..41f6e4a32dcfcaa8aa4743f3b3ebbb5a2058a133
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/CartesianTest.java
@@ -0,0 +1,24 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 迪卡尔积测试类
+ *
+ * @author zhaohuihua
+ * @version 20220206
+ */
+public class CartesianTest {
+
+    public static void main(String[] args) {
+        List<List<String>> sources = new ArrayList<>();
+        sources.add(StringTools.splits("A1|A2|A3"));
+        sources.add(StringTools.splits("B1|B2"));
+        sources.add(StringTools.splits("C1|C2|C3|C4"));
+        List<List<String>> targets = ConvertTools.toCartesianList(sources);
+        for (List<String> list : targets) {
+            System.out.println(ConvertTools.joinToString(list));
+        }
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/DateToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/DateToolsTest.java
index 35b20f06bb7e422cc263ebaad5fed460e4e938f9..c6d7a577a9809ca9191d74b33102ac14a92fcada 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/DateToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/DateToolsTest.java
@@ -28,6 +28,18 @@ public class DateToolsTest {
         testParse16("2018/12/24", "2018-12-24 00:00:00.000");
         testParse16("2016/2/29", "2016-02-29 00:00:00.000");
         testParse16("2018/2/29", "2018-03-01 00:00:00.000");
+        testParse16("2018-0-8", "2017-12-08 00:00:00.000");
+        testParse16("2018-7-8", "2018-07-08 00:00:00.000");
+        testParse16("2018-7-20", "2018-07-20 00:00:00.000");
+        testParse16("2018-12-24", "2018-12-24 00:00:00.000");
+        testParse16("2016-2-29", "2016-02-29 00:00:00.000");
+        testParse16("2018-2-29", "2018-03-01 00:00:00.000");
+        testParse16("2018.0.8", "2017-12-08 00:00:00.000");
+        testParse16("2018.7.8", "2018-07-08 00:00:00.000");
+        testParse16("2018.7.20", "2018-07-20 00:00:00.000");
+        testParse16("2018.12.24", "2018-12-24 00:00:00.000");
+        testParse16("2016.2.29", "2016-02-29 00:00:00.000");
+        testParse16("2018.2.29", "2018-03-01 00:00:00.000");
         testParse16("7/8/2018", "2018-07-08 00:00:00.000");
         testParse16("7/20/2018", "2018-07-20 00:00:00.000");
         testParse16("12/24/2018", "2018-12-24 00:00:00.000");
@@ -51,6 +63,14 @@ public class DateToolsTest {
         testParse30("2018/7/8 4:5:6.999", "2018-07-08 04:05:06.999");
         testParse30("2018/7/8 23:59:59.999999", "2018-07-08 23:59:59.999");
         testParse30("2018/7/8 23:59:59.9", "2018-07-08 23:59:59.900"); // 是900毫秒, 不009毫秒
+        testParse30("2018-7-8 20:30:40", "2018-07-08 20:30:40.000");
+        testParse30("2018-7-8 4:5:6.999", "2018-07-08 04:05:06.999");
+        testParse30("2018-7-8 23:59:59.999999", "2018-07-08 23:59:59.999");
+        testParse30("2018-7-8 23:59:59.9", "2018-07-08 23:59:59.900"); // 是900毫秒, 不009毫秒
+        testParse30("2018.7.8 20:30:40", "2018-07-08 20:30:40.000");
+        testParse30("2018.7.8 4:5:6.999", "2018-07-08 04:05:06.999");
+        testParse30("2018.7.8 23:59:59.999999", "2018-07-08 23:59:59.999");
+        testParse30("2018.7.8 23:59:59.9", "2018-07-08 23:59:59.900"); // 是900毫秒, 不009毫秒
         testParse35("20101020152535450+0000", "2010-10-20 23:25:35.450"); // yyyyMMddHHmmssSSSZ
         testParse35("20101020152535450GMT+00:00", "2010-10-20 23:25:35.450"); // yyyyMMddHHmmssSSSZ
         testParse35("2010-10-20 15:25:35.450 +0000", "2010-10-20 23:25:35.450"); // yyyy-MM-dd HH:mm:ss.SSS Z
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/DepthMapTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/DepthMapTest.java
index d28a21f6b185cc4a24b397ccfafbbfa70d9c044f..6af3f8e62e5d93437dd7f6acbecb37ff2de51717 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/DepthMapTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/DepthMapTest.java
@@ -14,7 +14,6 @@ public class DepthMapTest {
 
         dpm.put("code.folder.service", "service");
         dpm.put("code.folder.page", "views");
-        dpm.put("code.folder", "java"); // 该设置无效, 会被忽略掉
 
         System.out.println("author = " + JsonTools.toLogString(dpm.get("author"))); // Map({code=100, name=zhaohuihua})
         System.out.println("author.code = " + dpm.get("author.code")); // 100
@@ -24,5 +23,10 @@ public class DepthMapTest {
         System.out.println("code.folder.service = " + dpm.get("code.folder.service")); // service
         System.out.println("code.folder.page = " + dpm.get("code.folder.page")); // views
 
+        dpm.put("code.folder", "java"); // 该设置会覆盖掉code.folder.service和code.folder.page
+        System.out.println("code.folder = " + dpm.get("code.folder")); // views
+        System.out.println("code.folder.service = " + dpm.get("code.folder.service")); // service
+        System.out.println("code.folder.page = " + dpm.get("code.folder.page")); // views
+
     }
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/JsonTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/JsonTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b52c9d08907599f3f51ad6772ebd3c0c68d71a63
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/JsonTest.java
@@ -0,0 +1,31 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JsonTest
+ *
+ * @author zhaohuihua
+ * @version 20211209
+ */
+public class JsonTest {
+    public static void main(String[] args) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("accountCid", "201601251326023230619fe78f6220eb");
+        data.put("registerApplyCid", "2016012920303240607176aec6733a32");
+        data.put("status", "1");
+        Map<String, Object> params = new HashMap<>();
+        params.put("jsonData", data);
+        params.put("account", "15500000006");
+        params.put("accountType", 1);
+        params.put("appCode", "A000");
+        params.put("digest", "6ea2604fa0204421b2579aac82d43214");
+        params.put("imeiuuid", "359596063773059");
+        params.put("sourceType", "android");
+        params.put("ts", 945700);
+        params.put("createTime", DateTools.parse("2016-1-20"));
+        System.out.println(JsonTools.toLogString(params));
+        System.out.println(JsonTools.toJsonString(params));
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/KeyValueTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/KeyValueTest.java
index f4c41342aef8a45c517fb4d8bd7617f9362c53bd..6d89dcb61636e43661a6099c62425136d2958ba2 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/KeyValueTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/KeyValueTest.java
@@ -1,7 +1,6 @@
 package com.gitee.qdbp.tools.utils;
 
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
 
 public class KeyValueTest {
 
@@ -10,7 +9,7 @@ public class KeyValueTest {
         KeyValue<String> item = new KeyValue<>("kkk", "vvv");
         // fastjson-1.2.23.jar 输出: {"key":"kkk","value":"vvv"}
         // fastjson-1.2.24.jar 输出: {"kkk":"vvv"}
-        System.out.println(JSON.toJSONString(item));
+        System.out.println(JsonTools.toLogString(item));
     }
 
     protected static class KeyValue<V> implements Map.Entry<String, V> {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlCharacterTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlCharacterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3cb3ae65173078f023dbb6f24b1380304b9a5333
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlCharacterTest.java
@@ -0,0 +1,29 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import com.gitee.qdbp.able.exception.FormatException;
+
+/**
+ * OgnlCharapterTest
+ *
+ * @author zhaohuihua
+ * @version 20220306
+ */
+public class OgnlCharacterTest {
+
+    public static void main(String[] args) throws FormatException {
+        System.out.println();
+        Map<String, Object> map = new HashMap<>();
+        map.put("char1", 'A');
+        map.put("number1", (int) 'A');
+        map.put("string1", "A");
+        // 报错: Object result = OgnlTools.evaluate("string1==number1", map);
+        Object result = OgnlTools.evaluate("string1.equals(number1)", map);
+        System.out.println(result);
+        Object result2 = OgnlTools.evaluate("number1==char1", map);
+        System.out.println(result2);
+        Object result3 = OgnlTools.evaluate("number1==string1", map);
+        System.out.println(result3);
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlEvaluatorTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlEvaluatorTest.java
index 700476b2779a107c972d7dd223027750f0ced9a0..9270ea94a599884281ba0ebd89e2b9b3aabab16c 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlEvaluatorTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlEvaluatorTest.java
@@ -3,6 +3,8 @@ package com.gitee.qdbp.tools.utils;
 import java.math.BigDecimal;
 import java.util.HashMap;
 import java.util.Map;
+import org.testng.Assert;
+import org.testng.annotations.Test;
 import com.gitee.qdbp.able.model.reusable.StackWrapper;
 
 /**
@@ -11,9 +13,11 @@ import com.gitee.qdbp.able.model.reusable.StackWrapper;
  * @author zhaohuihua
  * @version 20210808
  */
+@Test
 public class OgnlEvaluatorTest {
 
-    public static void main(String[] args) {
+    @Test
+    public void test() {
         Map<String, Object> user = new HashMap<>();
         user.put("email", "abc@dd.com");
         Map<String, Object> map = new HashMap<>();
@@ -26,6 +30,7 @@ public class OgnlEvaluatorTest {
         // 再在表达式中使用
         Object isEmail = evaluator.evaluate("@StringTools.isEmail(user.email)", map);
         System.out.println("isEmail=" + isEmail);
+        Assert.assertEquals(isEmail, Boolean.TRUE, "isEmail");
 
         user.put("x", 100);
         user.put("y", 200);
@@ -34,9 +39,11 @@ public class OgnlEvaluatorTest {
         // 自定义函数sum来自CustomizedFunctions
         Object sum = evaluator.evaluate("@sum(user.x,user.y,user.z,300)", map);
         System.out.println("sum=" + sum);
+        Assert.assertEquals(sum, new BigDecimal("600"), "sum result");
         // 自定义函数has来自StackWrapper, 判断KEY是否存在
         Object has = evaluator.evaluate("@has(user.a.b.c)", map);
         System.out.println("has=" + has);
+        Assert.assertEquals(has, Boolean.FALSE, "@has(user.a.b.c)");
     }
 
     protected static class CustomizedFunctions extends StackWrapper {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlExpressionTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlExpressionTest.java
index 35ad50ff84dd4ee55559bc1e3f38feb570e66f7b..9edad6e5bb70d4bd7ec3cfb72c6a57517666c743 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlExpressionTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlExpressionTest.java
@@ -13,7 +13,7 @@ public class OgnlExpressionTest {
     public void test() throws Exception {
         { // Java精度问题: 0.9-1=-0.09999999999999998
             double result = 0.9 - 1;
-            System.out.printf("%-10s %s%n", "Java:", result);
+            System.out.printf("%-10s %s=%s%n", "Java:", "0.9-1", result);
         }
         { // Ognl支持BigDecimal: 0.9-1=-0.1
             String expression = "收入-支出";
@@ -21,7 +21,18 @@ public class OgnlExpressionTest {
             map.put("收入", 0.9);
             map.put("支出", 1);
             Object result = OgnlTools.evaluate(expression, map);
-            System.out.printf("%-10s %s=%s%n", "Ognl:", "0.9-1", result);
+            System.out.printf("%-10s %s=%s \t%s%n", "Ognl:", "0.9-1", result, expression);
+            Assert.assertEquals(result, new BigDecimal("-0.1"));
+        }
+        { // Ognl支持BigDecimal: 0.9-1=-0.1
+            String expression = "$2020年.收入-$2020年.支出";
+            Map<String, Number> map = new HashMap<>();
+            map.put("收入", 0.9);
+            map.put("支出", 1);
+            Map<String, Object> root = new HashMap<>();
+            root.put("$2020年", map);
+            Object result = OgnlTools.evaluate(expression, root);
+            System.out.printf("%-10s %s=%s \t%s%n", "Ognl:", "0.9-1", result, expression);
             Assert.assertEquals(result, new BigDecimal("-0.1"));
         }
         { // 加法
@@ -30,7 +41,7 @@ public class OgnlExpressionTest {
             map.put("x", 1);
             map.put("y", 3);
             Object result = OgnlTools.evaluate(expression, map);
-            System.out.printf("%-10s %s=%s%n", "加法:", "1+3", result);
+            System.out.printf("%-10s %s=%s \t%s%n", "加法:", "1+3", result, expression);
             Assert.assertEquals(result, new BigDecimal("4"));
         }
         { // 除法
@@ -39,7 +50,7 @@ public class OgnlExpressionTest {
             map.put("x", 1);
             map.put("y", 3);
             Object result = OgnlTools.evaluate(expression, 4, map);
-            System.out.printf("%-10s %s=%s%n", "除法:", "1/3", result);
+            System.out.printf("%-10s %s=%s \t%s%n", "除法:", "1/3", result, expression);
             Assert.assertEquals(result, new BigDecimal("0.3333"));
         }
         { // 复杂表达式: 速动比率=(流动资产-存货-预付账款)/流动负债
@@ -50,7 +61,7 @@ public class OgnlExpressionTest {
             map.put("预付账款", 98);
             map.put("流动负债", 289);
             Object result = OgnlTools.evaluate(expression, 4, map);
-            System.out.printf("%-10s %s%n", "速动比率:", result);
+            System.out.printf("%-10s %s \t%s%n", "速动比率:", result, expression);
             Assert.assertEquals(result, new BigDecimal("2.2042"));
         }
         { // 结果不能是科学计数法
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
index 11568788b7fa7b6325f8ef50a8697536b991b29a..95e8e39ea6fc967e8872c91a2eb0ddd3f20b9f78 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/OgnlToolsTest.java
@@ -30,13 +30,18 @@ public class OgnlToolsTest {
 
         String result1 = JsonTools.toLogString(map);
         System.out.println(result1);
-        Assert.assertEquals(result1, "{userBean:{address:{id:\"A0001\",name:\"Home\"}}}");
+        Map<String, Object> actual1 = JsonTools.beanToMap(map);
+        Map<String, Object> expected1 = JsonTools.parseAsMap("{userBean:{address:{id:\"A0001\",name:\"Home\"}}}");
+        AssertTools.assertDeepEquals(actual1, expected1);
         OgnlTools.setValue(map, "userBean.address.details", "Beijing China");
         OgnlTools.setValue(map, "test.address.id", "A0009");
         OgnlTools.setValue(map, "test.address.details", "Nanjing China");
         String result2 = JsonTools.toLogString(map);
         System.out.println(result2);
-        Assert.assertEquals(result2, "{test:{address:{id:\"A0009\",details:\"Nanjing China\"}},userBean:{address:{details:\"Beijing China\",id:\"A0001\",name:\"Home\"}}}");
+        Map<String, Object> actual2 = JsonTools.beanToMap(map);
+        Map<String, Object> expected2 = JsonTools.parseAsMap(
+                "{test:{address:{id:\"A0009\",details:\"Nanjing China\"}},userBean:{address:{details:\"Beijing China\",id:\"A0001\",name:\"Home\"}}}");
+        AssertTools.assertDeepEquals(actual2, expected2);
     }
 
     protected static class Address {
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/ParseParamsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/ParseParamsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ef2f12411f340bbc285306ce223f3210c01ef2e4
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/ParseParamsTest.java
@@ -0,0 +1,42 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.testng.annotations.Test;
+
+/**
+ * 解析参数测试类
+ *
+ * @author zhaohuihua
+ * @version 20220106
+ */
+@Test
+public class ParseParamsTest {
+
+    @Test
+    public void testParseJsonLikeParams() {
+        String s = "name:zhh; phone:139xxxx8888; code:&amp\\;aa&nbsp\\;bb&cc:dd;" +
+                " contentType:application/json\\;charset=UTF-8";
+        Map<String, String> expected = new HashMap<>();
+        expected.put("name", "zhh");
+        expected.put("phone", "139xxxx8888");
+        expected.put("code", "&amp;aa&nbsp;bb&cc:dd");
+        expected.put("contentType", "application/json;charset=UTF-8");
+        Map<String, String> actual = MapTools.parseJsonLikeParams(s);
+        System.out.println(actual);
+        AssertTools.assertDeepEquals(actual, expected);
+    }
+
+    @Test
+    public void testParseRequestParams() {
+        String s = "name=zhh&phone=139xxxx8888&code=&amp;aa&nbsp;bb\\&cc=dd&contentType=application/json;charset=UTF-8";
+        Map<String, String> expected = new HashMap<>();
+        expected.put("name", "zhh");
+        expected.put("phone", "139xxxx8888");
+        expected.put("code", "&amp;aa&nbsp;bb&cc=dd");
+        expected.put("contentType", "application/json;charset=UTF-8");
+        Map<String, String> actual = MapTools.parseRequestParams(s);
+        System.out.println(actual);
+        AssertTools.assertDeepEquals(actual, expected);
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/PathToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/PathToolsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1760ab5935e2414cbb8702d18841b12dd71ee9b3
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/PathToolsTest.java
@@ -0,0 +1,43 @@
+package com.gitee.qdbp.tools.utils;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import com.gitee.qdbp.tools.files.PathTools;
+
+/**
+ * PathTools测试类
+ *
+ * @author zhaohuihua
+ * @version 20230129
+ */
+@Test
+public class PathToolsTest {
+
+    @Test
+    public void getClasspathRelativePath1() {
+        String s1 = "jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties";
+        String r1 = PathTools.getClasspathRelativePath(s1);
+        Assert.assertEquals(r1, "settings/sqls/xxx.properties");
+    }
+
+    @Test
+    public void getClasspathRelativePath2() {
+        String s1 = "file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties";
+        String r1 = PathTools.getClasspathRelativePath(s1);
+        Assert.assertEquals(r1, "settings/sqls/xxx.properties");
+    }
+
+    @Test
+    public void getJarRelativePath1() {
+        String s1 = "jar:file:/E:/repository/com/gitee/qdbp/xxx-1.0.0.jar!/settings/sqls/xxx.properties";
+        String r1 = PathTools.getJarRelativePath(s1);
+        Assert.assertEquals(r1, "xxx-1.0.0.jar!/settings/sqls/xxx.properties");
+    }
+
+    @Test
+    public void getJarRelativePath2() {
+        String s1 = "file:/D:/qdbp/qdbp-jdbc/jdbc-core/target/classes/settings/sqls/xxx.properties";
+        String r1 = PathTools.getJarRelativePath(s1);
+        Assert.assertEquals(r1, "classes/settings/sqls/xxx.properties");
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
index 8a364779a2056a6a377dd8fb2ad5743f2fa3560c..05eaa848f38d684a0df7f037183dac4557c0c5e4 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/ReflectToolsTest.java
@@ -1,7 +1,8 @@
 package com.gitee.qdbp.tools.utils;
 
+import java.util.List;
 import java.util.Map;
-import com.alibaba.fastjson.JSON;
+import com.gitee.qdbp.able.beans.KeyValue;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -12,7 +13,7 @@ public class ReflectToolsTest {
     public void testGetDepthValue1() {
         String json = "{domain:{text:'baidu',url:'https://baidu.com'}}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "domain.text";
             String text = ReflectTools.getDepthValue(object, key);
@@ -38,7 +39,7 @@ public class ReflectToolsTest {
         String json =
                 "[{domain:{text:'baidu',url:'https://baidu.com'}},{domain:{text:'bing',url:'https://cn.bing.com'}}]";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMaps(json);
         {
             String key = "[1].domain.text";
             String text = ReflectTools.getDepthValue(object, key);
@@ -51,6 +52,18 @@ public class ReflectToolsTest {
             System.out.println(key + " = " + text);
             Assert.assertNull(text, key);
         }
+        {
+            String key = "[-1].domain.text";
+            String text = ReflectTools.getDepthValue(object, key);
+            System.out.println(key + " = " + text);
+            Assert.assertEquals(text, "bing", key);
+        }
+        {
+            String key = "[-2].domain.text";
+            String text = ReflectTools.getDepthValue(object, key);
+            System.out.println(key + " = " + text);
+            Assert.assertEquals(text, "baidu", key);
+        }
     }
 
     @Test
@@ -58,7 +71,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[1].domain";
             Map<String, Object> domain = ReflectTools.getDepthValue(object, key);
@@ -80,7 +93,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[1].domain.text";
             String text = ReflectTools.getDepthValue(object, key);
@@ -111,7 +124,7 @@ public class ReflectToolsTest {
     public void testGetLastListIndex() {
         String json = "{data:[1,2,3,4,5,6]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[0]";
             Integer result = ReflectTools.getDepthValue(object, key);
@@ -160,7 +173,7 @@ public class ReflectToolsTest {
     public void testContainsDepthFields1() {
         String json = "{data:[1,2,3,4,5,6]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[0]";
             boolean contains = ReflectTools.containsDepthFields(object, key);
@@ -210,7 +223,7 @@ public class ReflectToolsTest {
         String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
                 + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
         System.out.println(json);
-        Object object = JSON.parse(json);
+        Object object = JsonTools.parseAsMap(json);
         {
             String key = "data[1].domain";
             boolean contains = ReflectTools.containsDepthFields(object, key);
@@ -242,4 +255,296 @@ public class ReflectToolsTest {
             Assert.assertTrue(contains, key);
         }
     }
+
+    @Test
+    public void testFindDepthValue11() {
+        String json = "{domain:{text:'baidu',url:'https://baidu.com'}}";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMap(json);
+        {
+            String key = "domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "baidu", key);
+        }
+        {
+            String key = "domain.name";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "domain.name", key);
+            Assert.assertNull(values.get(0).getValue(), key);
+        }
+        {
+            String key = "domain.name || domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "baidu", key);
+        }
+    }
+
+    @Test
+    public void testFindDepthValue21() {
+        String json =
+                "[{domain:{text:'baidu',url:'https://baidu.com'}},{domain:{text:'bing',url:'https://cn.bing.com'}}]";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMaps(json);
+        {
+            String key = "[1].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "[1].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "bing", key);
+        }
+        {
+            String key = "[2].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 0, key + ".size");
+        }
+        {
+            String key = "[-1].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "[-1].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "bing", key);
+        }
+        {
+            String key = "[-2].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "[-2].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "baidu", key);
+        }
+    }
+
+    @Test
+    public void testFindDepthValue22() {
+        String json =
+                "[{domain:{text:'baidu',url:'https://baidu.com'}},{domain:{text:'bing',url:'https://cn.bing.com'}}]";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMaps(json);
+        String key = "domain.text";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        System.out.println(key + " = " + values);
+        Assert.assertEquals(values.size(), 2, key + ".size");
+        Assert.assertEquals(values.get(0).getKey(), "[0].domain.text", key);
+        Assert.assertEquals(values.get(0).getValue(), "baidu", key);
+        Assert.assertEquals(values.get(1).getKey(), "[1].domain.text", key);
+        Assert.assertEquals(values.get(1).getValue(), "bing", key);
+    }
+
+    @Test
+    public void testFindDepthValue31() {
+        String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
+                + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMap(json);
+        {
+            String key = "data[1].domain";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            Object value = values.get(0).getValue();
+            System.out.println(key + " = " + JsonTools.toLogString(value));
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain", key);
+            Assert.assertNotNull(value);
+            Assert.assertTrue(Map.class.isAssignableFrom(value.getClass()));
+            Map<?, ?> domain = (Map<?, ?>) value;
+            Assert.assertEquals(domain.get("text"), "bing", key + ".get('text')");
+            Assert.assertEquals(domain.get("url"), "https://cn.bing.com", key + ".get('url')");
+        }
+        {
+            String key = "data[2].domain";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + JsonTools.toLogString(values));
+            Assert.assertEquals(values.size(), 0, key);
+        }
+    }
+
+    @Test
+    public void testFindDepthValue32() {
+        String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
+                + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMap(json);
+        String key = "data.domain";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        Assert.assertEquals(values.size(), 2, key + ".size");
+
+        String key1 = values.get(0).getKey();
+        Object value1 = values.get(0).getValue();
+        System.out.println(key1 + " = " + JsonTools.toLogString(value1));
+        Assert.assertEquals(key1, "data[0].domain", key);
+        Assert.assertNotNull(value1);
+        Assert.assertTrue(Map.class.isAssignableFrom(value1.getClass()));
+        Map<?, ?> domain1 = (Map<?, ?>) value1;
+        Assert.assertEquals(domain1.get("text"), "baidu", key + ".get('text')");
+        Assert.assertEquals(domain1.get("url"), "https://baidu.com", key + ".get('url')");
+
+        String key2 = values.get(1).getKey();
+        Object value2 = values.get(1).getValue();
+        System.out.println(key2 + " = " + JsonTools.toLogString(value2));
+        Assert.assertEquals(key2, "data[1].domain", key);
+        Assert.assertNotNull(value2);
+        Assert.assertTrue(Map.class.isAssignableFrom(value2.getClass()));
+        Map<?, ?> domain2 = (Map<?, ?>) value2;
+        Assert.assertEquals(domain2.get("text"), "bing", key + ".get('text')");
+        Assert.assertEquals(domain2.get("url"), "https://cn.bing.com", key + ".get('url')");
+    }
+
+    @Test
+    public void testFindDepthValue41() {
+        String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com'}},"
+                + "{domain:{text:'bing',url:'https://cn.bing.com'}}]}";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMap(json);
+        {
+            String key = "data[1].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "bing", key);
+        }
+        {
+            String key = "data[1].domain.name";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain.name", key);
+            Assert.assertNull(values.get(0).getValue(), key);
+        }
+        {
+            String key = "data[1].domain.name || data[1].domain.text";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain.text", key);
+            Assert.assertEquals(values.get(0).getValue(), "bing", key);
+        }
+        {
+            String key = ".data[1].domain.name";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), "data[1].domain.name", key);
+            Assert.assertNull(values.get(0).getValue(), key);
+        }
+    }
+
+    @Test
+    public void testFindDepthValue51() {
+        String json = "{data:[{domain:{text:'baidu',url:'https://baidu.com',address:[{city:'Guangzhou'},{city:'Nanjing'}]}},"
+                + "{domain:{text:'bing',url:'https://cn.bing.com',address:[{city:'Beijing'},{city:'Shanghai'},{city:'Shenzhen'}]}}]}";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMap(json);
+        String key = "data.domain.address.city";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        System.out.println(key + " = " + values);
+        Assert.assertEquals(values.size(), 5, key + ".size");
+        Assert.assertEquals(values.get(0).getKey(), "data[0].domain.address[0].city", key);
+        Assert.assertEquals(values.get(0).getValue(), "Guangzhou", key);
+        Assert.assertEquals(values.get(1).getKey(), "data[0].domain.address[1].city", key);
+        Assert.assertEquals(values.get(1).getValue(), "Nanjing", key);
+        Assert.assertEquals(values.get(2).getKey(), "data[1].domain.address[0].city", key);
+        Assert.assertEquals(values.get(2).getValue(), "Beijing", key);
+        Assert.assertEquals(values.get(3).getKey(), "data[1].domain.address[1].city", key);
+        Assert.assertEquals(values.get(3).getValue(), "Shanghai", key);
+        Assert.assertEquals(values.get(4).getKey(), "data[1].domain.address[2].city", key);
+        Assert.assertEquals(values.get(4).getValue(), "Shenzhen", key);
+    }
+
+    @Test
+    public void testFindDepthValue61() {
+        String json = "{ data: [ { domain:{} }, { domain:null } ] }";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMap(json);
+        String key = "data.domain.text";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        System.out.println(key + " = " + values);
+        Assert.assertEquals(values.size(), 1, key + ".size");
+        Assert.assertEquals(values.get(0).getKey(), "data[0].domain.text", key);
+        Assert.assertNull(values.get(0).getValue(), key);
+    }
+
+    @Test
+    public void testFindDepthValue62() {
+        String json = "{ data: [] }";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMap(json);
+        String key = "data.domain.text";
+        List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+        System.out.println(key + " = " + values);
+        Assert.assertEquals(values.size(), 0, key + ".size");
+    }
+
+    @Test
+    public void testFindLastListIndex() {
+        String json = "{data:[1,2,3,4,5,6]}";
+        System.out.println(json);
+        Object object = JsonTools.parseAsMap(json);
+        {
+            String key = "data[0]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 1, key);
+        }
+        {
+            String key = "data[5]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 6, key);
+        }
+        {
+            String key = "data[-1]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 6, key);
+        }
+        {
+            String key = "data[-2]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 5, key);
+        }
+        {
+            String key = "data[-6]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertEquals(values.get(0).getValue(), 1, key);
+        }
+        {
+            String key = "data[+7]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertNull(values.get(0).getValue(), key);
+        }
+        {
+            String key = "data[-7]";
+            List<KeyValue<Object>> values = ReflectTools.findDepthValues(object, key);
+            System.out.println(key + " = " + values);
+            Assert.assertEquals(values.size(), 1, key + ".size");
+            Assert.assertEquals(values.get(0).getKey(), key, key);
+            Assert.assertNull(values.get(0).getValue(), key);
+        }
+    }
+
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/StringToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/StringToolsTest.java
index 861111703b337a70ce3a33c5403ac0f850801855..6d0f541e4938073e16436afdde41c02421f394d9 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/StringToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/StringToolsTest.java
@@ -1,5 +1,7 @@
 package com.gitee.qdbp.tools.utils;
 
+import java.util.Arrays;
+import java.util.List;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -143,4 +145,38 @@ public class StringToolsTest {
         Assert.assertFalse(StringTools.isNumber("9999,.9"));
         Assert.assertFalse(StringTools.isNumber("9,.9999"));
     }
+
+    public void testSplit() {
+        {
+            char[] delimiter = new char[] {'|'};
+            testSplit("aa|bb|cc", false, delimiter, "aa", "bb", "cc");
+            testSplit("aa|bb||cc", false, delimiter, "aa", "bb", "", "cc");
+            testSplit("aa\\tbb||cc|\\\\dd", false, delimiter, "aa\\tbb", "", "cc", "\\\\dd");
+            testSplit("aa\\|bb||cc|\\\\dd", false, delimiter, "aa\\", "bb", "", "cc", "\\\\dd");
+        }
+        {
+            char[] delimiter = new char[] {'|'};
+            testSplit("aa|bb|cc", true, delimiter, "aa", "bb", "cc");
+            testSplit("aa|bb||cc", true, delimiter, "aa", "bb", "", "cc");
+            testSplit("aa\\tbb||cc|\\\\dd", true, delimiter, "aa\\tbb", "", "cc", "\\dd");
+            testSplit("aa\\|bb||cc|\\\\dd", true, delimiter, "aa|bb", "", "cc", "\\dd");
+        }
+        {
+            char[] delimiter = new char[] {'|', '\t'};
+            testSplit("aa\\tbb||cc|\\\\dd", false, delimiter, "aa\\tbb", "", "cc", "\\\\dd");
+        }
+        {
+            char[] delimiter = new char[] {'|', '\t'};
+            testSplit("aa\\tbb||cc|\\\\dd", true, delimiter, "aa\tbb", "", "cc", "\\dd");
+        }
+    }
+
+    private void testSplit(String source, boolean escape, char[] delimiter, String... targets) {
+        List<String> result = StringTools.splits(source, true, escape, delimiter);
+        System.out.println("escape=" + escape);
+        System.out.println(source);
+        System.out.println(ConvertTools.joinToString(result, ", "));
+        AssertTools.assertDeepEquals(result, Arrays.asList(targets));
+        System.out.println("================");
+    }
 }
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/UrlGetTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/UrlGetTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ebd132822887ab2ffb3651a7465f8372450f699a
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/UrlGetTest.java
@@ -0,0 +1,27 @@
+package com.gitee.qdbp.tools.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+public class UrlGetTest {
+
+    public static void main(String[] args) throws IOException {
+        System.out.println(getUrlContent("https://www.bing.com"));
+    }
+
+    public static String getUrlContent(String url) throws IOException {
+        try (InputStream input = new URL(url).openStream();
+                ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+            int length;
+            byte[] buffer = new byte[1024];
+            while ((length = input.read(buffer, 0, buffer.length)) > 0) {
+                output.write(buffer, 0, length);
+            }
+            byte[] bytes = output.toByteArray();
+            return new String(bytes, StandardCharsets.UTF_8);
+        }
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/VerifyToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/VerifyToolsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0510c5a329c5395401927ae8e693e67bcdb21140
--- /dev/null
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/VerifyToolsTest.java
@@ -0,0 +1,58 @@
+package com.gitee.qdbp.tools.utils;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * VerifyToolsTest
+ *
+ * @author zhaohuihua
+ * @version 20230327
+ */
+@Test
+public class VerifyToolsTest {
+
+    @Test
+    public void testJsonObjectString() {
+        {
+            boolean result = VerifyTools.isJsonObjectString("{}");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonObjectString("{  }");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonObjectString("{a:'11'}");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonObjectString("{aaa}");
+            Assert.assertFalse(result);
+        }
+    }
+
+    @Test
+    public void testJsonArrayString() {
+        {
+            boolean result = VerifyTools.isJsonArrayString("{}");
+            Assert.assertFalse(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonArrayString("[]");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonArrayString("[  ]");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonArrayString("[1,2,3]");
+            Assert.assertTrue(result);
+        }
+        {
+            boolean result = VerifyTools.isJsonArrayString("[ {a:'11'} ]");
+            Assert.assertTrue(result);
+        }
+    }
+}
diff --git a/tools/src/test/java/com/gitee/qdbp/tools/utils/XmlToolsTest.java b/tools/src/test/java/com/gitee/qdbp/tools/utils/XmlToolsTest.java
index ed3d8b6e684c30bef29fd974c5bf872f0dc8c630..87c981e291e5fb7bd7971900fdc0edcd3bb7be37 100644
--- a/tools/src/test/java/com/gitee/qdbp/tools/utils/XmlToolsTest.java
+++ b/tools/src/test/java/com/gitee/qdbp/tools/utils/XmlToolsTest.java
@@ -3,11 +3,11 @@ package com.gitee.qdbp.tools.utils;
 import java.io.IOException;
 import java.net.URL;
 import java.util.Map;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.gitee.qdbp.tools.files.PathTools;
-import com.gitee.qdbp.tools.utils.XmlTools.XmlToJsonOptions;
 import org.testng.Assert;
 import org.testng.annotations.Test;
+import com.gitee.qdbp.json.JsonFeature;
+import com.gitee.qdbp.tools.files.PathTools;
+import com.gitee.qdbp.tools.utils.XmlTools.XmlToJsonOptions;
 
 @Test
 public class XmlToolsTest {
@@ -131,9 +131,10 @@ public class XmlToolsTest {
 
     private void testXmlToJson(String xml, String json, XmlToJsonOptions options) {
         Object result = XmlTools.xmlToJson(xml, options);
-        String actual = JsonTools.toLogString(result,
-            // 使用单引号, 输出Map中的null值
-            SerializerFeature.UseSingleQuotes, SerializerFeature.WriteMapNullValue);
+        // 使用单引号, 输出Map中的null值
+        JsonFeature.Serialization feature = new JsonFeature.Serialization()
+                .setQuoteFieldNames(false).setUseSingleQuotes(true).setWriteMapNullValue(true);
+        String actual = JsonTools.toJsonString(result, feature);
         System.out.println("--------------------------------");
         System.out.println(xml);
         System.out.println(json);