woody hace 1 año
padre
commit
2d07702e26

+ 4 - 3
framework-common/src/main/java/com/chelvc/framework/common/util/ExcelUtils.java

@@ -10,7 +10,6 @@ import java.time.LocalDateTime;
 import java.util.Calendar;
 import java.util.Collection;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -18,6 +17,8 @@ import java.util.Map;
 import java.util.TreeSet;
 import java.util.function.BiFunction;
 
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 import lombok.NonNull;
 import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
@@ -474,12 +475,12 @@ public final class ExcelUtils {
         /**
          * 单元格列下标列表(从小到大排序)
          */
-        private final TreeSet<Integer> columns = new TreeSet<>();
+        private final TreeSet<Integer> columns = Sets.newTreeSet();
 
         /**
          * 单元格列下标/单元格映射表
          */
-        private final Map<Integer, Cell> cells = new HashMap<>();
+        private final Map<Integer, Cell> cells = Maps.newHashMap();
 
         /**
          * 行下标(从0开始)

+ 4 - 5
framework-common/src/main/java/com/chelvc/framework/common/util/ObjectUtils.java

@@ -18,7 +18,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -69,23 +68,23 @@ public final class ObjectUtils {
     /**
      * 对象字段映射表
      */
-    private static final Map<Class<?>, Field[]> CLASS_FIELD_MAPPING = new ConcurrentHashMap<>();
+    private static final Map<Class<?>, Field[]> CLASS_FIELD_MAPPING = Maps.newConcurrentMap();
 
     /**
      * SerializedLambda 反序列化缓存
      */
-    private static final Map<String, SerializedLambda> LAMBDA_FUNCTION_MAPPING = new ConcurrentHashMap<>();
+    private static final Map<String, SerializedLambda> LAMBDA_FUNCTION_MAPPING = Maps.newConcurrentMap();
 
     /**
      * Protostuff对象类型/Schema映射表
      */
-    private static final Map<Class<?>, Schema<?>> PROTOSTUFF_CLASS_SCHEMA_MAPPING = new ConcurrentHashMap<>();
+    private static final Map<Class<?>, Schema<?>> PROTOSTUFF_CLASS_SCHEMA_MAPPING = Maps.newConcurrentMap();
 
     /**
      * 类对象/属性描述映射表
      */
     private static final Map<Class<?>, List<PropertyDescriptor>> CLASS_PROPERTY_DESCRIPTOR_MAPPING =
-            new ConcurrentHashMap<>();
+            Maps.newConcurrentMap();
 
     static {
         // 注册简单的对象拷贝转换器

+ 2 - 2
framework-common/src/main/java/com/chelvc/framework/common/util/StringUtils.java

@@ -5,7 +5,6 @@ import java.util.Comparator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
@@ -17,6 +16,7 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.google.common.collect.Maps;
 import lombok.NonNull;
 
 /**
@@ -181,7 +181,7 @@ public final class StringUtils {
     /**
      * 正则表达式/匹配模式对象映射表
      */
-    private static final Map<String, Pattern> REGEX_PATTERN_MAPPING = new ConcurrentHashMap<>();
+    private static final Map<String, Pattern> REGEX_PATTERN_MAPPING = Maps.newConcurrentMap();
 
     private StringUtils() {
     }

+ 27 - 4
framework-database/src/main/java/com/chelvc/framework/database/context/DatabaseContextHolder.java

@@ -27,7 +27,6 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -67,6 +66,7 @@ import com.chelvc.framework.database.support.EventService;
 import com.google.common.collect.Maps;
 import lombok.NonNull;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.ibatis.type.TypeHandler;
 import org.mybatis.spring.SqlSessionTemplate;
 import org.springframework.context.ApplicationContext;
 import org.springframework.dao.DuplicateKeyException;
@@ -79,20 +79,25 @@ import org.springframework.util.CollectionUtils;
  * @date 2024/1/30
  */
 public final class DatabaseContextHolder {
+    /**
+     * 类对象/类型处理器实例映射表
+     */
+    private static final Map<Class<?>, TypeHandler<?>> TYPE_HANDLER_MAPPING = Maps.newConcurrentMap();
+
     /**
      * 类对象/业务操作实例映射表
      */
-    private static final Map<Class<?>, IService<Object>> CLASS_SERVICE_MAPPING = new ConcurrentHashMap<>();
+    private static final Map<Class<?>, IService<Object>> CLASS_SERVICE_MAPPING = Maps.newConcurrentMap();
 
     /**
      * 对象/唯一约束条件列表映射
      */
-    private static final Map<Class<?>, List<UniqueContext<?>>> CLASS_UNIQUE_MAPPING = new ConcurrentHashMap<>();
+    private static final Map<Class<?>, List<UniqueContext<?>>> CLASS_UNIQUE_MAPPING = Maps.newConcurrentMap();
 
     /**
      * 对象加密数据字段映射表
      */
-    private static final Map<Class<?>, List<Field>> CLASS_CONFIDENTIAL_FIELD_MAPPING = new ConcurrentHashMap<>();
+    private static final Map<Class<?>, List<Field>> CLASS_CONFIDENTIAL_FIELD_MAPPING = Maps.newConcurrentMap();
 
     /**
      * 配置属性
@@ -560,6 +565,24 @@ public final class DatabaseContextHolder {
         }
     }
 
+    /**
+     * 初始化TypeHandler实例
+     *
+     * @param clazz TypeHandler对象类型
+     * @param <T>   TypeHandler类型
+     * @return TypeHandler实例
+     */
+    @SuppressWarnings("unchecked")
+    public static <T extends TypeHandler<?>> T initializeTypeHandler(@NonNull Class<T> clazz) {
+        return (T) TYPE_HANDLER_MAPPING.computeIfAbsent(clazz, c -> {
+            try {
+                return clazz.newInstance();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
     /**
      * 根据包名查找数据模型对象
      *

+ 20 - 9
framework-database/src/main/java/com/chelvc/framework/database/interceptor/JsonHandlerConfigureInterceptor.java

@@ -7,6 +7,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
 
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
@@ -68,6 +69,16 @@ import org.w3c.dom.Element;
  */
 @Slf4j
 public class JsonHandlerConfigureInterceptor extends MybatisConfigureInterceptor {
+    /**
+     * JSON类型处理器计数器
+     */
+    private final AtomicLong counter = new AtomicLong(0);
+
+    /**
+     * JSON类型/处理器映射表
+     */
+    private final Map<Type, Class<?>> handlers = Maps.newConcurrentMap();
+
     /**
      * 内置的别名/对象类型映射表
      */
@@ -169,11 +180,13 @@ public class JsonHandlerConfigureInterceptor extends MybatisConfigureInterceptor
                 }
             }
         }
-        try {
-            return this.initializeJsonHandlerClass(field);
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
+        return this.handlers.computeIfAbsent(type, t -> {
+            try {
+                return this.initializeJsonHandlerClass(field);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
     }
 
     /**
@@ -185,14 +198,12 @@ public class JsonHandlerConfigureInterceptor extends MybatisConfigureInterceptor
      */
     private Class<?> initializeJsonHandlerClass(Field field) throws Exception {
         ClassPool pool = ClassPool.getDefault();
-        CtClass handler = pool.makeClass(String.format(
-                "%s.JsonTypeHandler_%s", field.getDeclaringClass().getPackage().getName(), StringUtils.uuid()
-        ));
+        CtClass handler = pool.makeClass(String.format("_JsonTypeHandler_%d", this.counter.incrementAndGet()));
         handler.setSuperclass(pool.get(JsonTypeHandler.Simple.class.getName()));
         CtConstructor constructor = new CtConstructor(new CtClass[]{}, handler);
         constructor.setBody(String.format("{super(%s);}", ObjectUtils.analyse(field.getGenericType())));
         handler.addConstructor(constructor);
-        return handler.toClass();
+        return handler.toClass(this.getClass().getClassLoader(), null);
     }
 
     /**

+ 77 - 52
framework-database/src/main/java/com/chelvc/framework/database/interceptor/PropertyUpdateInterceptor.java

@@ -11,6 +11,7 @@ import java.util.Objects;
 import java.util.stream.Collectors;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
 import com.baomidou.mybatisplus.core.metadata.TableInfo;
 import com.baomidou.mybatisplus.core.toolkit.StringPool;
 import com.chelvc.framework.base.context.SessionContextHolder;
@@ -28,6 +29,7 @@ import lombok.extern.slf4j.Slf4j;
 import net.sf.jsqlparser.JSQLParserException;
 import net.sf.jsqlparser.expression.Alias;
 import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.JdbcParameter;
 import net.sf.jsqlparser.expression.LongValue;
 import net.sf.jsqlparser.expression.StringValue;
 import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
@@ -46,6 +48,7 @@ import org.apache.ibatis.executor.Executor;
 import org.apache.ibatis.executor.statement.StatementHandler;
 import org.apache.ibatis.mapping.BoundSql;
 import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.ParameterMapping;
 import org.apache.ibatis.mapping.SqlCommandType;
 import org.apache.ibatis.plugin.Interceptor;
 import org.apache.ibatis.plugin.Intercepts;
@@ -55,6 +58,8 @@ import org.apache.ibatis.plugin.Signature;
 import org.apache.ibatis.reflection.SystemMetaObject;
 import org.apache.ibatis.session.ResultHandler;
 import org.apache.ibatis.session.RowBounds;
+import org.apache.ibatis.type.TypeHandler;
+import org.apache.ibatis.type.UnknownTypeHandler;
 import org.springframework.stereotype.Component;
 
 /**
@@ -133,13 +138,29 @@ public class PropertyUpdateInterceptor implements Interceptor {
     }
 
     /**
-     * 初始化创建信息默认值
+     * 绑定参数TypeHandler
      *
-     * @param creatable 创建信息
+     * @param table       表信息
+     * @param bound       绑定SQL
+     * @param columns     字段列表
+     * @param expressions 表达式列表
      */
-    private void initializeCreatableValue(Creatable<?> creatable) {
-        creatable.setCreator(SessionContextHolder.getId());
-        creatable.setCreateTime(new Date());
+    private void bindTypeHandler(TableInfo table, BoundSql bound, List<Column> columns, List<Expression> expressions) {
+        List<ParameterMapping> mappings = bound.getParameterMappings();
+        Map<String, TableFieldInfo> fields = table.getFieldList().stream()
+                .collect(Collectors.toMap(TableFieldInfo::getColumn, field -> field));
+        for (int i = 0; i < columns.size(); i++) {
+            Column column = columns.get(i);
+            Expression expression = expressions.get(i);
+            Class<? extends TypeHandler<?>> clazz = fields.get(column.getColumnName()).getTypeHandler();
+            if (clazz != null && expression instanceof JdbcParameter) {
+                ParameterMapping mapping = mappings.get(((JdbcParameter) expression).getIndex() - 1);
+                if (mapping.getTypeHandler() == null || mapping.getTypeHandler() instanceof UnknownTypeHandler) {
+                    TypeHandler<?> handler = DatabaseContextHolder.initializeTypeHandler(clazz);
+                    SystemMetaObject.forObject(mapping).setValue("typeHandler", handler);
+                }
+            }
+        }
     }
 
     /**
@@ -155,8 +176,8 @@ public class PropertyUpdateInterceptor implements Interceptor {
         }
 
         // 设置创建人字段值
-        boolean containCreator = this.isContains(insert.getColumns(), Expressions.CREATOR_COLUMN);
-        if (!containCreator) {
+        boolean includeCreator = !this.isContains(insert.getColumns(), Expressions.CREATOR_COLUMN);
+        if (includeCreator) {
             insert.addColumns(Expressions.CREATOR_COLUMN);
             Expression operator = this.operator();
             ItemsList items = insert.getItemsList();
@@ -168,8 +189,8 @@ public class PropertyUpdateInterceptor implements Interceptor {
         }
 
         // 设置创建时间字段值
-        boolean containCreateTime = this.isContains(insert.getColumns(), Expressions.CREATE_TIME_COLUMN);
-        if (!containCreateTime) {
+        boolean includeCreateTime = !this.isContains(insert.getColumns(), Expressions.CREATE_TIME_COLUMN);
+        if (includeCreateTime) {
             insert.addColumns(Expressions.CREATE_TIME_COLUMN);
             Expression datetime = this.datetime();
             ItemsList items = insert.getItemsList();
@@ -179,17 +200,7 @@ public class PropertyUpdateInterceptor implements Interceptor {
                 ((ExpressionList) items).getExpressions().add(datetime);
             }
         }
-        return !(containCreator && containCreateTime);
-    }
-
-    /**
-     * 初始化修改信息默认值
-     *
-     * @param updatable 修改信息
-     */
-    private void initializeUpdatableValue(Updatable<?> updatable) {
-        updatable.setUpdater(SessionContextHolder.getId());
-        updatable.setUpdateTime(new Date());
+        return includeCreator || includeCreateTime;
     }
 
     /**
@@ -205,8 +216,8 @@ public class PropertyUpdateInterceptor implements Interceptor {
         }
 
         // 设置更新人字段值
-        boolean containUpdater = this.isContains(insert.getColumns(), Expressions.UPDATER_COLUMN);
-        if (!containUpdater) {
+        boolean includeUpdater = !this.isContains(insert.getColumns(), Expressions.UPDATER_COLUMN);
+        if (includeUpdater) {
             insert.addColumns(Expressions.UPDATER_COLUMN);
             Expression operator = this.operator();
             ItemsList items = insert.getItemsList();
@@ -218,8 +229,8 @@ public class PropertyUpdateInterceptor implements Interceptor {
         }
 
         // 设置更新时间字段值
-        boolean containUpdateTime = this.isContains(insert.getColumns(), Expressions.UPDATE_TIME_COLUMN);
-        if (!containUpdateTime) {
+        boolean includeUpdateTime = !this.isContains(insert.getColumns(), Expressions.UPDATE_TIME_COLUMN);
+        if (includeUpdateTime) {
             insert.addColumns(Expressions.UPDATE_TIME_COLUMN);
             Expression datetime = this.datetime();
             ItemsList items = insert.getItemsList();
@@ -229,7 +240,7 @@ public class PropertyUpdateInterceptor implements Interceptor {
                 ((ExpressionList) items).getExpressions().add(datetime);
             }
         }
-        return !(containUpdater && containUpdateTime);
+        return includeUpdater || includeUpdateTime;
     }
 
     /**
@@ -246,19 +257,21 @@ public class PropertyUpdateInterceptor implements Interceptor {
 
         // 设置更新人字段值
         Table table = update.getTable();
-        boolean containUpdater = this.isContains(table, update.getColumns(), Expressions.UPDATER_COLUMN);
-        if (!containUpdater) {
+        Expression operator = this.operator();
+        boolean includeUpdater = operator != Expressions.NULL_VALUE
+                && !this.isContains(table, update.getColumns(), Expressions.UPDATER_COLUMN);
+        if (includeUpdater) {
             if (StringUtils.isEmpty(table.getAlias())) {
                 update.addColumns(Expressions.UPDATER_COLUMN);
             } else {
                 update.addColumns(new Column(table, Expressions.UPDATER_COLUMN.getColumnName()));
             }
-            update.addExpressions(this.operator());
+            update.addExpressions(operator);
         }
 
         // 设置更新时间字段值
-        boolean containUpdateTime = this.isContains(table, update.getColumns(), Expressions.UPDATE_TIME_COLUMN);
-        if (!containUpdateTime) {
+        boolean includeUpdateTime = !this.isContains(table, update.getColumns(), Expressions.UPDATE_TIME_COLUMN);
+        if (includeUpdateTime) {
             if (StringUtils.isEmpty(table.getAlias())) {
                 update.addColumns(Expressions.UPDATE_TIME_COLUMN);
             } else {
@@ -266,16 +279,7 @@ public class PropertyUpdateInterceptor implements Interceptor {
             }
             update.addExpressions(this.datetime());
         }
-        return !(containUpdater && containUpdateTime);
-    }
-
-    /**
-     * 初始化删除信息默认值
-     *
-     * @param deletable 删除信息
-     */
-    private void initializeDeletableValue(Deletable<?> deletable) {
-        deletable.setDeleted(false);
+        return includeUpdater || includeUpdateTime;
     }
 
     /**
@@ -369,19 +373,34 @@ public class PropertyUpdateInterceptor implements Interceptor {
             throw new RuntimeException(e);
         }
 
-        // 构建带默认值SQL语句
+        // 重置SQL语句
         StringBuilder sql = null;
         for (Statement statement : statements.getStatements()) {
             boolean changed = false;
             if (statement instanceof Insert) {
+                // 绑定参数TypeHandler
                 Insert insert = (Insert) statement;
                 TableInfo table = DatabaseContextHolder.getTable(insert.getTable().getName());
+                ItemsList items = insert.getItemsList();
+                if (items instanceof MultiExpressionList) {
+                    List<Expression> expressions = ((MultiExpressionList) items).getExpressionLists()
+                            .stream().flatMap(el -> el.getExpressions().stream()).collect(Collectors.toList());
+                    this.bindTypeHandler(table, bound, insert.getColumns(), expressions);
+                } else {
+                    this.bindTypeHandler(table, bound, insert.getColumns(), ((ExpressionList) items).getExpressions());
+                }
+
+                // 初始化参数默认值
                 Class<?> type = ObjectUtils.ifNull(table, TableInfo::getEntityType);
                 changed = type != null && (this.initializeCreatableValue(type, insert)
                         || this.initializeUpdatableValue(type, insert) || this.initializeDeletableValue(type, insert));
             } else if (statement instanceof Update) {
+                // 绑定参数TypeHandler
                 Update update = (Update) statement;
                 TableInfo table = DatabaseContextHolder.getTable(update.getTable().getName());
+                this.bindTypeHandler(table, bound, update.getColumns(), update.getExpressions());
+
+                // 初始化参数默认值
                 Class<?> type = ObjectUtils.ifNull(table, TableInfo::getEntityType);
                 changed = type != null && this.initializeUpdatableValue(type, update);
             }
@@ -395,8 +414,6 @@ public class PropertyUpdateInterceptor implements Interceptor {
                 sql.append(statement.toString());
             }
         }
-
-        // 重置SQL语句
         if (sql != null) {
             SystemMetaObject.forObject(bound).setValue("sql", sql.toString());
         }
@@ -413,18 +430,26 @@ public class PropertyUpdateInterceptor implements Interceptor {
     @SuppressWarnings("unchecked")
     private <T extends Entity<?>> void modify(T entity, SqlCommandType operation) {
         // 初始化字段默认值
-        if (operation == SqlCommandType.INSERT) {
-            if (entity instanceof Creatable) {
-                this.initializeCreatableValue((Creatable<?>) entity);
-            }
+        if (entity instanceof Creatable || entity instanceof Updatable || entity instanceof Deletable) {
+            Date now = new Date();
+            Long operator = SessionContextHolder.getId();
             if (entity instanceof Updatable) {
-                this.initializeUpdatableValue((Updatable<?>) entity);
+                if (operator != null) {
+                    ((Updatable<?>) entity).setUpdater(operator);
+                }
+                ((Updatable<?>) entity).setUpdateTime(now);
             }
-            if (entity instanceof Deletable) {
-                this.initializeDeletableValue((Deletable<?>) entity);
+            if (operation == SqlCommandType.INSERT) {
+                if (entity instanceof Creatable) {
+                    if (operator != null) {
+                        ((Creatable<?>) entity).setCreator(operator);
+                    }
+                    ((Creatable<?>) entity).setCreateTime(now);
+                }
+                if (entity instanceof Deletable) {
+                    ((Deletable<?>) entity).setDeleted(false);
+                }
             }
-        } else if (operation == SqlCommandType.UPDATE && entity instanceof Updatable) {
-            this.initializeUpdatableValue((Updatable<?>) entity);
         }
 
         // 设置加密属性值

+ 56 - 23
framework-email/src/main/java/com/chelvc/framework/email/LogbackEmailAppender.java

@@ -2,6 +2,8 @@ package com.chelvc.framework.email;
 
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import ch.qos.logback.classic.Level;
 import ch.qos.logback.classic.filter.ThresholdFilter;
@@ -13,8 +15,9 @@ import ch.qos.logback.core.spi.FilterReply;
 import com.chelvc.framework.base.util.SpringUtils;
 import com.chelvc.framework.common.util.StringUtils;
 import com.google.common.collect.Maps;
-import lombok.Getter;
+import lombok.AccessLevel;
 import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeansException;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
@@ -26,7 +29,7 @@ import org.springframework.stereotype.Component;
  * @author Woody
  * @date 2024/5/20
  */
-@Getter
+@Slf4j
 @Setter
 @Component
 public class LogbackEmailAppender extends AppenderBase<ILoggingEvent> implements ApplicationContextAware {
@@ -38,9 +41,14 @@ public class LogbackEmailAppender extends AppenderBase<ILoggingEvent> implements
     private String to;
     private String subject;
     private long interval = 60000;
-    private boolean asynchronous = true;
     private Layout<ILoggingEvent> layout;
-    private final Map<Integer, Long> hashes = Maps.newConcurrentMap();
+
+
+    private final Lock lock = new ReentrantLock();
+    private final Map<Integer, Long> caches = Maps.newConcurrentMap();
+
+    @Setter(AccessLevel.NONE)
+    private volatile long cleaned = System.currentTimeMillis();
 
     /**
      * 判断日志邮件发送器是否已准备就绪
@@ -61,9 +69,9 @@ public class LogbackEmailAppender extends AppenderBase<ILoggingEvent> implements
     }
 
     /**
-     * 获取事件hash值
+     * 获取日志事件hash值
      *
-     * @param event 事件对象实例
+     * @param event 日志事件对象实例
      * @return hash值
      */
     protected int hash(ILoggingEvent event) {
@@ -73,18 +81,33 @@ public class LogbackEmailAppender extends AppenderBase<ILoggingEvent> implements
     }
 
     /**
-     * 判断是否是重复事件
+     * 判断是否是重复日志事件
      *
-     * @param event 事件对象实例
+     * @param event 日志事件对象实例
      * @return true/false
      */
     protected boolean isDuplicated(ILoggingEvent event) {
+        if (this.interval < 1) {
+            return false;
+        }
         int hash = this.hash(event);
         long now = System.currentTimeMillis();
-        Long timestamp = this.hashes.put(hash, now);
+        Long timestamp = this.caches.put(hash, now);
         return timestamp != null && now - timestamp <= this.interval;
     }
 
+    /**
+     * 发送日志邮件
+     *
+     * @param event 日志事件对象实例
+     */
+    protected void send(ILoggingEvent event) {
+        String type = this.layout.getContentType();
+        String content = this.layout.doLayout(event);
+        Body body = Body.builder().to(this.to).type(type).subject(this.subject).content(content).build();
+        this.getEmailHandler().send(body);
+    }
+
     @Override
     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
         EMAIL_HANDLER = applicationContext.getBean(EmailHandler.class);
@@ -92,22 +115,32 @@ public class LogbackEmailAppender extends AppenderBase<ILoggingEvent> implements
 
     @Override
     protected void append(ILoggingEvent event) {
-        if (!this.isReady() || (event.getLevel() == Level.ERROR && this.isDuplicated(event))) {
+        if (!this.isReady() || LogbackEmailAppender.class.getName().equals(event.getLoggerName())
+                || (event.getLevel() == Level.ERROR && this.isDuplicated(event))) {
             return;
         }
 
-        // 构建邮件体
-        String type = this.layout.getContentType();
-        String content = this.layout.doLayout(event);
-        Body body = Body.builder().to(this.to).type(type).subject(this.subject).content(content).build();
-
         // 异步发送邮件
-        EmailHandler handler = this.getEmailHandler();
-        if (this.asynchronous) {
-            this.context.getScheduledExecutorService().execute(() -> handler.send(body));
-        } else {
-            handler.send(body);
-        }
+        this.context.getScheduledExecutorService().execute(() -> {
+            try {
+                this.send(event);
+            } catch (Exception e) {
+                log.warn("Email send failed: {}", e.getMessage());
+            } finally {
+                // 清理日志缓存
+                if (this.interval > 0) {
+                    long now = System.currentTimeMillis();
+                    if (now - this.cleaned > this.interval && !this.caches.isEmpty() && this.lock.tryLock()) {
+                        try {
+                            this.caches.entrySet().removeIf(entry -> now - entry.getValue() > this.interval);
+                            this.cleaned = now;
+                        } finally {
+                            this.lock.unlock();
+                        }
+                    }
+                }
+            }
+        });
     }
 
     /**
@@ -118,9 +151,9 @@ public class LogbackEmailAppender extends AppenderBase<ILoggingEvent> implements
         private String[] excludes;
 
         /**
-         * 判断事件是否满足邮件发送条件
+         * 判断日志事件是否满足邮件发送条件
          *
-         * @param event 事件对象实例
+         * @param event 日志事件对象实例
          * @return true/false
          */
         protected boolean matches(ILoggingEvent event) {

+ 6 - 1
framework-email/src/main/java/com/chelvc/framework/email/context/EmailContextHolder.java

@@ -33,6 +33,11 @@ import lombok.NonNull;
  * @date 2024/4/3
  */
 public final class EmailContextHolder {
+    /**
+     * 默认内容类型
+     */
+    public static final String DEFAULT_MIME_TYPE = "text/plain";
+
     static {
         FileTypeMap.setDefaultFileTypeMap(FileTypeMap.getDefaultFileTypeMap());
     }
@@ -48,10 +53,10 @@ public final class EmailContextHolder {
      */
     public static Multipart part(@NonNull Body body) {
         // 构建邮件内容部分
-        String type = body.getType();
         String text = body.getContent();
         MimeMultipart part = new MimeMultipart();
         MimeBodyPart content = new MimeBodyPart();
+        String type = StringUtils.ifEmpty(body.getType(), DEFAULT_MIME_TYPE);
         try {
             if (ContentTypeUtil.isTextual(type)) {
                 content.setText(text, StandardCharsets.UTF_8.name(), ContentTypeUtil.getSubType(type));