首页 > 其他分享 >[笔记] 使用注解切面(AOP)实现操作日志和数据日志记录

[笔记] 使用注解切面(AOP)实现操作日志和数据日志记录

时间:2024-10-14 16:46:44浏览次数:3  
标签:null return String private 切面 AOP 日志 param public

前言

  • 只是一个笔记,肯定有不足的地方,麻烦指出来一起进步.
  • 因为是多模块的内部项目,所以不会有高并发.
  • 所以是在一个线程内进行的

一.枚举

  • 操作状态
/**
 * 操作状态
 */
public enum BusinessStatus {
    /**
     * 成功
     */
    SUCCESS,

    /**
     * 失败
     */
    FAIL,
}

  • 业务操作类型
/**
 * 业务操作类型
 */
public enum BusinessType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 授权
     */
    GRANT,

    /**
     * 导出
     */
    EXPORT,

    /**
     * 导入
     */
    IMPORT,

    /**
     * 强退
     */
    FORCE,

    /**
     * 生成代码
     */
    GENCODE,

    /**
     * 清空数据
     */
    CLEAN,
}
  • 操作人类别
/**
 * 操作人类别
 */
public enum OperatorType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 后台用户
     */
    MANAGE,

    /**
     * 手机端用户
     */
    MOBILE
}

二.工具类

  • ThreadLocalIdUtils
/**
 * ThreadLocalIdUtils 是一个工具类,用于在每个线程内管理唯一的 ID。
 * 通过 ThreadLocal 存储 ID,确保每个线程都有自己独立的 ID 副本,
 * 避免线程间的变量冲突。
 *
 * @author 鲁子狄
 **/
public class ThreadLocalIdUtils {
    /**
     * ThreadLocal 变量用于存储当前线程的 ID。
     */
    private static final ThreadLocal<Long> threadId = new ThreadLocal<>();

    /**
     * 私有构造函数,防止外部实例化此类。
     * 此类仅包含静态方法,不应被实例化。
     *
     * @throws UnsupportedOperationException 如果试图实例化此工具类时抛出此异常。
     */
    private ThreadLocalIdUtils() {
        throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
    }

    /**
     * 设置当前线程的 ID。
     *
     * @param id 要设置的 ID 值。
     */
    public static void setId(Long id) {
        ThreadLocalIdUtils.threadId.set(id);
    }

    /**
     * 获取当前线程的 ID。
     *
     * @return 当前线程的 ID,如果没有设置则返回默认值。
     */
    public static Long getId() {
        return ThreadLocalIdUtils.threadId.get();
    }

    /**
     * 清除当前线程的 ID。
     * 通常在请求结束后调用此方法,释放 ThreadLocal 占用的资源。
     */
    public static void clearId() {
        ThreadLocalIdUtils.threadId.remove();
    }
}
  • FieldUtils
/**
 * 字段工具类
 *
 * @author 鲁子狄
 **/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("unchecked")
public class FieldUtils {

    /**
     * 获取指定对象的指定字段的值。
     *
     * @param obj          对象
     * @param fieldName    字段名
     * @param entityType   实体类型
     * @param specialFields 特殊处理的字段集合
     * @param <T>          返回类型的泛型
     * @return 字段值
     */
    public static <T> T getField(Object obj, String fieldName, Class<?> entityType, Set<String> specialFields) {

        Class<?> clazz = obj.getClass();

        // 如果对象类型不匹配,进行类型转换
        if (!clazz.equals(entityType)) {
            obj = MapstructUtils.convert(obj, entityType);
            clazz = entityType;
        }

        while (clazz != null) {
            try {
                // 获取字段
                Field field = clazz.getDeclaredField(fieldName);
                // 设置访问权限
                field.setAccessible(true);
                // 获取字段值
                return (T) field.get(obj);
            } catch (NoSuchFieldException e) {
                // 如果字段在特殊处理字段集合中,则返回 null
                if (specialFields != null && specialFields.contains(fieldName)) {
                    return null;
                }
                // 继续向上级类中查找
                clazz = clazz.getSuperclass();
            } catch (IllegalAccessException e) {
                throw new RuntimeException("无法访问字段 " + fieldName + ":" + e);
            }
        }

        // 如果到达这里,说明没有找到字段
        throw new NoSuchElementException("无法找到字段 " + fieldName);
    }
}
  • JsonUtils
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JsonUtils {
	/**
     * compareJsonFields 比较两个 JSON 对象的字段,并返回差异
     *
     * @param oldValuesJson 旧值
     * @param newValuesJson 新值
     * @param excludedFields 排除字段列表
     * @return {@link java.lang.String}
     */
    public static String compareJsonFields(String oldValuesJson, String newValuesJson,Set<String> excludedFields) {

        ObjectMapper mapper = new ObjectMapper();

        try {
            JsonNode oldNode = mapper.readTree(oldValuesJson);
            JsonNode newNode = mapper.readTree(newValuesJson);

            ObjectNode differences = mapper.createObjectNode();

            // 遍历旧的 JSON 节点
            oldNode.fieldNames().forEachRemaining(fieldName -> {

                // 如果字段在排除列表中,则跳过
                if (excludedFields!=null && excludedFields.contains(fieldName)){
                    return;
                }

                JsonNode oldValue = oldNode.get(fieldName);
                JsonNode newValue = newNode.get(fieldName);

                // 如果字段值不同,添加到 differences 中
                if (!oldValue.equals(newValue)) {
                    ObjectNode fieldDifferences = mapper.createObjectNode();
                    fieldDifferences.putPOJO("old", oldValue);
                    fieldDifferences.putPOJO("new", newValue);
                    differences.set(fieldName, fieldDifferences);
                }
            });

            // 检查新 JSON 中独有的字段
            newNode.fieldNames().forEachRemaining(fieldName -> {
                if (!oldNode.has(fieldName)) {
                    ObjectNode fieldDifferences = mapper.createObjectNode();
                    fieldDifferences.set("old", null);
                    fieldDifferences.set("new", newNode.get(fieldName));
                    differences.set(fieldName, fieldDifferences);
                }
            });

            // 返回差异的 JSON 字符串
            return differences.toString();

        } catch (IOException e) {
            JsonUtils.log.error("解析JSON出错", e);
            return null;
        }
    }
}

三.操作日志

1.表结构

create table sys_oper_log
(
    oper_id        bigint                         not null comment '日志主键'
        primary key,
    tenant_id      varchar(20)   default '000000' null comment '租户编号',
    title          varchar(50)   default ''       null comment '模块标题',
    business_type  int           default 0        null comment '业务类型(0其它 1新增 2修改 3删除)',
    method         varchar(100)  default ''       null comment '方法名称',
    request_method varchar(10)   default ''       null comment '请求方式',
    operator_type  int           default 0        null comment '操作类别(0其它 1后台用户 2手机端用户)',
    oper_name      varchar(50)   default ''       null comment '操作人员',
    dept_name      varchar(50)   default ''       null comment '部门名称',
    oper_url       varchar(255)  default ''       null comment '请求URL',
    oper_ip        varchar(128)  default ''       null comment '主机地址',
    oper_location  varchar(255)  default ''       null comment '操作地点',
    oper_param     varchar(2000) default ''       null comment '请求参数',
    json_result    varchar(2000) default ''       null comment '返回参数',
    status         int           default 0        null comment '操作状态(0正常 1异常)',
    error_msg      varchar(2000) default ''       null comment '错误消息',
    oper_time      datetime                       null comment '操作时间',
    cost_time      bigint        default 0        null comment '消耗时间'
)
    comment '操作日志记录';

2.注解

/**
 * 自定义操作日志记录注解
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 模块
     */
    String title() default "";

    /**
     * 功能
     */
    BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    boolean isSaveRequestData() default true;

    /**
     * 是否保存响应的参数
     */
    boolean isSaveResponseData() default true;


    /**
     * 排除指定的请求参数
     */
    String[] excludeParamNames() default {};

}

3.日志事件

  • 操作日志事件
/**
 * 操作日志事件
 */

@Data
public class OperLogEvent implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 日志主键
     */
    private Long operId;

    /**
     * 租户ID
     */
    private String tenantId;

    /**
     * 操作模块
     */
    private String title;

    /**
     * 业务类型(0其它 1新增 2修改 3删除)
     */
    private Integer businessType;

    /**
     * 业务类型数组
     */
    private Integer[] businessTypes;

    /**
     * 请求方法
     */
    private String method;

    /**
     * 请求方式
     */
    private String requestMethod;

    /**
     * 操作类别(0其它 1后台用户 2手机端用户)
     */
    private Integer operatorType;

    /**
     * 操作人员
     */
    private String operName;

    /**
     * 部门名称
     */
    private String deptName;

    /**
     * 请求url
     */
    private String operUrl;

    /**
     * 操作地址
     */
    private String operIp;

    /**
     * 操作地点
     */
    private String operLocation;

    /**
     * 请求参数
     */
    private String operParam;

    /**
     * 返回参数
     */
    private String jsonResult;

    /**
     * 操作状态(0正常 1异常)
     */
    private Integer status;

    /**
     * 错误消息
     */
    private String errorMsg;

    /**
     * 操作时间
     */
    private Date operTime;

    /**
     * 消耗时间
     */
    private Long costTime;
}
  • 登录事件
/**
 * 登录事件
 */
@Data
public class LogininforEvent implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 租户ID
     */
    private String tenantId;

    /**
     * 用户账号
     */
    private String username;

    /**
     * 登录状态 0成功 1失败
     */
    private String status;

    /**
     * 提示消息
     */
    private String message;

    /**
     * 请求体
     */
    private HttpServletRequest request;

    /**
     * 其他参数
     */
    private Object[] args;

}

4.切面

/**
 * 操作日志记录处理
 */
@Slf4j
@Aspect
@AutoConfiguration
public class LogAspect {

    /**
     * 排除敏感属性字段
     */
    public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };


    /**
     * 计时 key
     */
    private static final ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();

    /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(controllerLog)")
    public void doBefore(JoinPoint joinPoint, Log controllerLog) {
        ThreadLocalIdUtils.setId(IdWorker.getId());
        StopWatch stopWatch = new StopWatch();
        KEY_CACHE.set(stopWatch);
        stopWatch.start();
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
        handleLog(joinPoint, controllerLog, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {

            // *========数据库日志=========*//
            OperLogEvent operLog = new OperLogEvent();
            operLog.setOperId(ThreadLocalIdUtils.getId());
            operLog.setTenantId(LoginHelper.getTenantId());
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = ServletUtils.getClientIP();
            operLog.setOperIp(ip);
            operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
            LoginUser loginUser = LoginHelper.getLoginUser();
            operLog.setOperName(loginUser.getUsername());
            operLog.setDeptName(loginUser.getDeptName());

            if (e != null) {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 设置消耗时间
            StopWatch stopWatch = KEY_CACHE.get();
            stopWatch.stop();
            operLog.setCostTime(stopWatch.getTime());
            // 发布事件保存数据库
            SpringUtils.context().publishEvent(operLog);
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        } finally {
            KEY_CACHE.remove();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param log     日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperLogEvent operLog, Object jsonResult) throws Exception {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setOperatorType(log.operatorType().ordinal());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息,传入到数据库中。
            setRequestValue(joinPoint, operLog, log.excludeParamNames());
        }
        // 是否需要保存response,参数和值
        if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
            operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数,放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog, String[] excludeParamNames) throws Exception {
        Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        String requestMethod = operLog.getRequestMethod();
        if (MapUtil.isEmpty(paramsMap)
                && HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
            String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
        } else {
            MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
            MapUtil.removeAny(paramsMap, excludeParamNames);
            operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
        }
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
        StringJoiner params = new StringJoiner(" ");
        if (ArrayUtil.isEmpty(paramsArray)) {
            return params.toString();
        }
        for (Object o : paramsArray) {
            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
                String str = JsonUtils.toJsonString(o);
                Dict dict = JsonUtils.parseMap(str);
                if (MapUtil.isNotEmpty(dict)) {
                    MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
                    MapUtil.removeAny(dict, excludeParamNames);
                    str = JsonUtils.toJsonString(dict);
                }
                params.add(str);
            }
        }
        return params.toString();
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象,则返回true;否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.values()) {
                return value instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
               || o instanceof BindingResult;
    }
}

5. 事件监听器

	/**
     * 操作日志记录
     *
     * @param operLogEvent 操作日志事件
     */
    @Async
    @EventListener
    public void recordOper(OperLogEvent operLogEvent) {
        SysOperLogBo operLog = MapstructUtils.convert(operLogEvent, SysOperLogBo.class);
        // 远程查询操作地点
        operLog.setOperLocation(AddressUtils.getRealAddressByIp(operLog.getOperIp()));
        insertOperlog(operLog);
    }
    
   /**
     * 新增操作日志
     *
     * @param bo 操作日志对象
     */
    @Override
    public void insertOperlog(SysOperLogBo bo) {
        SysOperLog operLog = MapstructUtils.convert(bo, SysOperLog.class);
        operLog.setOperTime(new Date());
        baseMapper.insert(operLog);
    }

四. 数据日志

1.表结构

create table sys_data_log
(
    data_id        bigint                        not null comment '数据日志ID'
        primary key,
    tenant_id      varchar(20)  default '000000' null comment '租户编号',
    oper_id        bigint                        null comment '关联的操作日志ID',
    table_name     varchar(255) default ''       null comment '受影响的数据表名',
    row_id         varchar(255) default ''       null comment '受影响的数据行ID或主键',
    old_values     text                          null comment '变更前的值(JSON格式)',
    new_values     text                          null comment '变更后的值(JSON格式)',
    change_columns text                          null comment '变更的列(JSON格式)',
    change_type    int          default 0        null comment '变更类型(如:0其它 1插入 2更新 3删除)',
    oper_time      datetime                      null comment '操作时间',
    oper_name      varchar(50)  default ''       null comment '操作人员',
    dept_name      varchar(50)  default ''       null comment '部门名称'
)
    comment '数据日志记录';

2.注解

/**
 * 自定义数据日志注解
 *
 * @author 鲁子狄
 **/
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataLog {

    /**
     * 功能
     */
    BusinessType businessType() default BusinessType.OTHER;

    /**
     * mapper class
     */
    Class<? extends BaseMapperPlus> baseMapper() default BaseMapperPlus.class;
}

3.日志事件

/**
 * 数据日志事件
 *
 * @author 鲁子狄
 **/
@Data
public class DataLogEvent implements Serializable {
    @Serial
    private static final long serialVersionUID = 8640083616912539107L;
    /**
     * 数据日志ID
     */
    private Long dataId;

    /**
     * 关联的操作日志ID
     */
    private Long operId;

    /**
     * 受影响的数据表名
     */
    private String tableName;

    /**
     * 受影响的数据行ID或主键
     */
    private String rowId;

    /**
     * 变更前的值(JSON格式)
     */
    private String oldValues;

    /**
     * 变更后的值(JSON格式)
     */
    private String newValues;

    /**
     * 变更的列(JSON格式)
     */
    private String changeColumns;

    /**
     * 变更类型(如:0其它 1插入 2更新 3删除)
     */
    private Integer changeType;

    /**
     * 操作人员
     */
    private String operName;

    /**
     * 部门名称
     */
    private String deptName;
}

4.切面

/**
 * 数据日志处理
 *
 * @author 鲁子狄
 **/
@Slf4j
@Aspect
@AutoConfiguration
@SuppressWarnings("unchecked")
public class DataLogAspect {

    /**
     * 创建一个 Set 来存储需要排除的字段名称
     */
    private static final Set<String> EXCLUDED_FIELDS =
        Set.of("version", "createDept", "createTime", "createBy",
            "updateTime", "updateBy", "delFlag", "tenantId");

    /**
     * 环绕拦截
     *
     * @param joinPoint  切点
     * @param serviceLog 注解
     * @return Object
     * @throws Throwable 错误
     */
    @Around("@annotation(serviceLog)")
    public static Object doAround(ProceedingJoinPoint joinPoint, DataLog serviceLog) throws Throwable {

        // 获取对应的 BaseMapperPlus 实例
        BaseMapperPlus<?, ?> baseMapper = SpringUtil.getBean(serviceLog.baseMapper());
        // 获取实体类类型
        Class<?> entityType = baseMapper.currentModelClass();

        // 根据业务类型处理不同的操作
        return switch (serviceLog.businessType()) {
            case INSERT ->
                // 插入操作
                DataLogAspect.handleInsert(joinPoint, entityType);
            case UPDATE ->
                // 更新操作
                DataLogAspect.handleUpdate(joinPoint, entityType, baseMapper);
            case DELETE ->
                // 删除操作
                DataLogAspect.handleDelete(joinPoint, entityType, baseMapper);
            default ->
                // 默认情况下执行原方法
                joinPoint.proceed();
        };
    }

    /**
     * 处理插入操作
     *
     * @param joinPoint 切点
     * @return Object
     * @throws Throwable 错误
     */
    private static Object handleInsert(ProceedingJoinPoint joinPoint, Class<?> entityType) throws Throwable {
        // 执行原方法
        Object result = joinPoint.proceed();
        // 记录插入日志
        DataLogAspect.handInsert(joinPoint, result, entityType);
        return result;
    }

    /**
     * 处理更新操作
     *
     * @param joinPoint 切点
     * @return Object
     * @throws Throwable 错误
     */
    private static Object handleUpdate(ProceedingJoinPoint joinPoint, Class<?> entityType, BaseMapperPlus<?, ?> baseMapper) throws Throwable {
        // 获取方法参数
        Object arg = joinPoint.getArgs()[0];
        // 获取主键ID
        Long id = DataLogAspect.getIdField(arg, entityType);
        // 获取旧数据的JSON表示
        String oldValuesJson = DataLogAspect.getOldValuesJson(id, entityType, baseMapper);
        // 执行原方法
        Object result = joinPoint.proceed();
        // 获取新数据的JSON表示
        String newValuesJson = DataLogAspect.getValuesJson(arg, entityType);
        // 比较新旧数据
        String compareJson = JsonUtils.compareJsonFields(oldValuesJson, newValuesJson, DataLogAspect.EXCLUDED_FIELDS);
        // 如果数据有变化
        if (StringUtils.isNotBlank(compareJson) && !"{}".equals(compareJson)) {
            // 记录更新日志
            DataLogAspect.handUpdate(oldValuesJson, newValuesJson, compareJson, id, entityType);
        }
        return result;
    }

    /**
     * 处理删除操作
     *
     * @param joinPoint 切点
     * @return Object
     * @throws Throwable 错误
     */
    private static Object handleDelete(ProceedingJoinPoint joinPoint, Class<?> entityType, BaseMapperPlus<?, ?> baseMapper) throws Throwable {
        // 执行原方法
        Object result = joinPoint.proceed();
        // 记录删除日志
        DataLogAspect.handDelete((Collection<Long>) joinPoint.getArgs()[0], entityType, baseMapper);
        return result;
    }

    /**
     * 处理插入日志
     *
     * @param joinPoint 切点
     * @param id        插入后的主键ID
     */
    private static void handInsert(ProceedingJoinPoint joinPoint, Object id, Class<?> entityType) {
        // 创建日志事件
        DataLogEvent dataLog = DataLogAspect.createDataLogEvent(BusinessType.INSERT, entityType);
        // 设置新数据
        dataLog.setNewValues(DataLogAspect.getValuesJson(joinPoint.getArgs()[0], entityType));
        // 设置主键ID字符串
        dataLog.setRowId(String.valueOf(id));
        // 发布事件
        DataLogAspect.publishEvent(dataLog);
    }

    /**
     * 处理更新日志
     *
     * @param oldValuesJson 旧数据的JSON表示
     * @param newValuesJson 新数据的JSON表示
     * @param compareJson   比较结果的JSON表示
     * @param id            主键ID
     */
    private static void handUpdate(String oldValuesJson, String newValuesJson, String compareJson, Long id, Class<?> entityType) {
        // 创建日志事件
        DataLogEvent dataLog = DataLogAspect.createDataLogEvent(BusinessType.UPDATE, entityType);
        // 设置旧数据
        dataLog.setOldValues(oldValuesJson);
        // 设置新数据
        dataLog.setNewValues(newValuesJson);
        // 设置变更列
        dataLog.setChangeColumns(compareJson);
        // 设置主键ID字符串
        dataLog.setRowId(String.valueOf(id));
        // 发布事件
        DataLogAspect.publishEvent(dataLog);
    }

    /**
     * 处理删除日志
     *
     * @param ids 被删除的数据的主键ID集合
     */
    private static void handDelete(Collection<Long> ids, Class<?> entityType, BaseMapperPlus<?, ?> baseMapper) {
        if (ids.isEmpty()){
            return;
        }
        // 创建日志事件
        DataLogEvent dataLog = DataLogAspect.createDataLogEvent(BusinessType.DELETE, entityType);
        // 设置主键ID字符串
        dataLog.setRowId(StringUtils.join(ids, ", "));
        // 设置数据
        dataLog.setOldValues(JsonUtils.toJsonString(DataLogAspect.getDeletedList(ids, baseMapper)));
        // 发布事件
        DataLogAspect.publishEvent(dataLog);
    }

    /**
     * getDeletedList 获取已删除的数据
     *
     * @param ids 主键id集合
     * @return {@link java.util.List<?>}
     */
    private static List<?> getDeletedList(Collection<Long> ids, BaseMapperPlus<?, ?> baseMapper) {
        QueryWrapper queryWrapper = new QueryWrapper<>();
        queryWrapper.in("id", ids);
        String sql = String.format(
            "OR (id IN (%s) AND del_flag = 2)",
            StringUtils.join(ids, ", ")
        );
        queryWrapper.last(sql);
        return baseMapper.selectList(queryWrapper);
    }

    /**
     * 创建 DataLogEvent 对象
     *
     * @param businessType 业务类型
     * @return DataLogEvent
     */
    private static DataLogEvent createDataLogEvent(BusinessType businessType, Class<?> entityType) {

        DataLogEvent dataLog = new DataLogEvent();
        // 设置操作者ID
        dataLog.setOperId(ThreadLocalIdUtils.getId());
        // 设置表名
        dataLog.setTableName(DataLogAspect.getTableName(entityType));
        // 设置变更类型
        dataLog.setChangeType(businessType.ordinal());
        // 获取登录用户信息
        LoginUser loginUser = LoginHelper.getLoginUser();
        // 设置操作者用户名
        dataLog.setOperName(loginUser.getUsername());
        // 设置部门名
        dataLog.setDeptName(loginUser.getDeptName());
        return dataLog;
    }

    /**
     * 获取表名
     *
     * @return 表名
     */
    private static String getTableName(Class<?> entityType) {
        // 检查类是否具有 @TableName 注解
        if (entityType.isAnnotationPresent(TableName.class)) {
            // 获取注解的 value 属性值
            return entityType.getAnnotation(TableName.class).value();
        } else {
            // 抛出异常
            throw new RuntimeException("实体类上没有 @TableName 注解");
        }
    }

    /**
     * 获取旧数据的 JSON 表示
     *
     * @param id 主键 ID
     * @return JSON 字符串
     */
    private static String getOldValuesJson(Long id, Class<?> entityType, BaseMapperPlus<?, ?> baseMapper) {
        // 通过主键获取旧数据
        Object result = baseMapper.selectById(id);
        // 将旧数据转换为 JSON 字符串
        return DataLogAspect.getValuesJson(result, entityType);
    }

    /**
     * 获取对象的 JSON 表示
     *
     * @param obj 对象
     * @return JSON 字符串
     */
    private static String getValuesJson(Object obj, Class<?> entityType) {
        // 如果对象类型不匹配
        if (!obj.getClass().equals(entityType)) {
            // 转换对象类型
            obj = MapstructUtils.convert(obj, entityType);
        }
        // 将对象转换为 JSON 字符串
        return JsonUtils.toJsonString(obj);
    }

    /**
     * getIdField 获取指定对象的 id 字段的值
     *
     * @param obj 对象
     * @return {@link T}
     */
    private static <T> T getIdField(Object obj, Class<?> entityType) {
        // 获取指定对象的指定字段的值
        return FieldUtils.getField(obj, "id", entityType, null);
    }

    /**
     * 发布事件保存数据库
     *
     * @param dataLog 数据日志事件
     */
    private static void publishEvent(DataLogEvent dataLog) {
        // 发布事件
        SpringUtils.context().publishEvent(dataLog);
    }

}

5. 事件监听器

	 /**
     * 操作日志记录
     *
     * @param dataLogEvent 数据日志事件
     */
    @Async
    @EventListener
    public void recordData(DataLogEvent dataLogEvent) {
        insertByBo(MapstructUtils.convert(dataLogEvent, SysDataLogBo.class));
    }
    
 	 /**
     * insertByBo 新增数据日志记录
     *
     * @param bo 数据日志记录
     * @return {@link Long} 主键
     */
    @Override
    public Long insertByBo(SysDataLogBo bo) {
        SysDataLog add = MapstructUtils.convert(bo, SysDataLog.class);
        add.setOperTime(LocalDateTime.now());
        return baseMapper.insert(add) > 0 ? add.getDataId() : null;
    }

五.使用

1.操作日志

/**
 * 基础单据信息表(base_document)表控制层
 *
 * @author 鲁子狄
 */
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/base/document")
public class BaseDocumentController extends BaseController {

    private final IBaseDocumentService baseDocumentService;

    /**
     * 分页查询基础单据信息列表
     */
    @SaCheckPermission("base:document:page")
    @GetMapping("/page")
    public TableDataInfo<BaseDocumentVo> page(BaseDocumentBo bo, PageQuery pageQuery) {
        return baseDocumentService.queryPageList(bo, pageQuery);
    }

    /**
     * 查询基础单据信息列表
     */
    @SaCheckPermission("base:document:list")
    @GetMapping("/list")
    public R<List<BaseDocumentVo>> list(BaseDocumentBo bo) {
        return R.ok(baseDocumentService.queryList(bo));
    }

    /**
     * 导出基础单据信息列表
     */
    @SaCheckPermission("base:document:export")
    @Log(title = "单据管理", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(BaseDocumentBo bo, HttpServletResponse response) {
        List<BaseDocumentVo> list = baseDocumentService.queryList(bo);
        ExcelUtil.exportExcel(list, "基础单据信息", BaseDocumentVo.class, response);
    }

    /**
     * 获取基础单据信息详细信息
     */
    @SaCheckPermission("base:document:query")
    @GetMapping("/{id}")
    public R<BaseDocumentVo> getInfo(@NotNull(message = "主键不能为空")
                                     @PathVariable Long id) {
        return R.ok(baseDocumentService.queryById(id));
    }

    /**
     * 新增基础单据信息
     */
    @SaCheckPermission("base:document:add")
    @Log(title = "单据管理", businessType = BusinessType.INSERT)
    @RepeatSubmit()
    @PostMapping()
    public R<Long> add(@Validated(AddGroup.class) @RequestBody BaseDocumentBo bo) {
        return R.ok(baseDocumentService.insertByBo(bo));
    }

    /**
     * 修改基础单据信息
     */
    @SaCheckPermission("base:document:edit")
    @Log(title = "单据管理", businessType = BusinessType.UPDATE)
    @RepeatSubmit()
    @PutMapping()
    public R<Void> edit(@Validated(EditGroup.class) @RequestBody BaseDocumentBo bo) {
        return toAjax(baseDocumentService.updateByBo(bo));
    }

    /**
     * 删除基础单据信息
     */
    @SaCheckPermission("base:document:remove")
    @Log(title = "单据管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    public R<Void> remove(@NotEmpty(message = "主键不能为空")
                          @PathVariable Long[] ids) {
        return toAjax(baseDocumentService.deleteWithValidByIds(List.of(ids), true));
    }
}

2.数据日志

/**
 * 基础单据信息 Service业务层处理
 *
 * @author 鲁子狄
 */
@RequiredArgsConstructor
@Service
public class BaseDocumentServiceImpl extends ServiceImpl<BaseDocumentMapper, BaseDocument> implements IBaseDocumentService {

    private final IBaseDocAssocService baseDocAssocService;

    /**
     * buildQueryWrapper  构建条件构造器
     *
     * @param bo bo
     * @return {@link LambdaQueryWrapper<BaseDocument>} 条件构造器
     */
    private static LambdaQueryWrapper<BaseDocument> buildQueryWrapper(BaseDocumentBo bo) {
        LambdaQueryWrapper<BaseDocument> lqw = Wrappers.lambdaQuery();
        lqw.like(StringUtils.isNotBlank(bo.getDocumentName()), BaseDocument::getDocumentName, bo.getDocumentName());
        lqw.like(StringUtils.isNotBlank(bo.getModuleName()), BaseDocument::getModuleName, bo.getModuleName());
        lqw.eq(StringUtils.isNotBlank(bo.getStatus()), BaseDocument::getStatus, bo.getStatus());
        return lqw;
    }

    /**
     * queryById 查询基础单据信息
     *
     * @param id 主键
     * @return {@link BaseDocumentVo} 基础单据信息
     */
    @Override
    public BaseDocumentVo queryById(Long id) {
        return baseMapper.selectVoById(id);
    }

    /**
     * queryPageList 分页查询基础单据信息列表
     *
     * @param bo        查询条件
     * @param pageQuery 分页参数
     * @return {@link TableDataInfo<BaseDocumentVo>} 基础单据信息分页列表
     */
    @Override
    public TableDataInfo<BaseDocumentVo> queryPageList(BaseDocumentBo bo, PageQuery pageQuery) {
        LambdaQueryWrapper<BaseDocument> lqw = buildQueryWrapper(bo);
        Page<BaseDocumentVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
        return TableDataInfo.build(result);
    }

    /**
     * queryList 查询符合条件的基础单据信息列表
     *
     * @param bo 查询条件
     * @return {@link List<BaseDocumentVo>} 基础单据信息列表
     */
    @Override
    public List<BaseDocumentVo> queryList(BaseDocumentBo bo) {
        LambdaQueryWrapper<BaseDocument> lqw = buildQueryWrapper(bo);
        return baseMapper.selectVoList(lqw);
    }

    /**
     * insertByBo 新增基础单据信息
     *
     * @param bo 基础单据信息
     * @return {@link Long} 主键
     */
    @Override
    @DataLog(businessType= BusinessType.INSERT, baseMapper = BaseDocumentMapper.class)
    @Transactional(rollbackFor = Exception.class)
    public Long insertByBo(BaseDocumentBo bo) {
        BaseDocument add = MapstructUtils.convert(bo, BaseDocument.class);
        validEntityBeforeSave(add);
        return baseMapper.insert(add) > 0 ? add.getId() : null;
    }

    /**
     * updateByBo 修改基础单据信息
     *
     * @param bo 基础单据信息
     * @return {@link Boolean} 是否修改成功
     */
    @Override
    @DataLog(businessType= BusinessType.UPDATE, baseMapper = BaseDocumentMapper.class)
    @Transactional(rollbackFor = Exception.class)
    public Boolean updateByBo(BaseDocumentBo bo) {
        BaseDocument update = MapstructUtils.convert(bo, BaseDocument.class);
        validEntityBeforeSave(update);
        return baseMapper.updateById(update) > 0;
    }


    /**
     * validEntityBeforeSave 保存前的数据校验
     *
     * @param entity entity
     */
    private void validEntityBeforeSave(BaseDocument entity) {
        LambdaQueryWrapper<BaseDocument> lqw = Wrappers.lambdaQuery();
        lqw.eq(BaseDocument::getDocumentName, entity.getDocumentName());
        if (ObjectUtils.isNotEmpty(entity.getId()) && !entity.getId().equals(0L)) {
            lqw.ne(BaseDocument::getId, entity.getId());
        }
        if (baseMapper.selectCount(lqw) > 0) {
            throw new ServiceException("单据名称已存在.");
        }
    }

    /**
     * deleteWithValidByIds 校验并批量删除基础单据信息信息
     *
     * @param ids     待删除的主键集合
     * @param isValid 是否进行有效性校验
     * @return {@link Boolean} 是否删除成功
     */
    @Override
    @DataLog(businessType= BusinessType.DELETE, baseMapper = BaseDocumentMapper.class)
    @Transactional(rollbackFor = Exception.class)
    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
        baseDocAssocService.remove(new LambdaQueryWrapper<BaseDocAssoc>().in(BaseDocAssoc::getDocId, ids));
        return baseMapper.deleteByIds(ids) > 0;
    }
}

标签:null,return,String,private,切面,AOP,日志,param,public
From: https://blog.csdn.net/LuChangQiu/article/details/142921371

相关文章

  • jar包内替换依赖jar后无法启动,错误日志:It has been compressed and nested jar files
    jar包内替换依赖jar后无法启动,错误日志:Ithasbeencompressedandnestedjarfilesmustbestoredwithoutcompression.ruoyi、springboot、java、jar、libs、压缩背景某服务jar包足足90MB有余,远程传输太慢,目前在改动的是其中的某子jar(项目内部依赖,另一个jar)。之前......
  • jar包内替换依赖jar后无法启动,错误日志:It has been compressed and nested jar files
    jar包内替换依赖jar后无法启动,错误日志:Ithasbeencompressedandnestedjarfilesmustbestoredwithoutcompression.ruoyi、springboot、java、jar、libs、压缩背景某服务jar包足足90MB有余,远程传输太慢,目前在改动的是其中的某子jar(项目内部依赖,另一个jar)。之前......
  • Spring学习——SpringAOP
    0.IOC思想(DI)1.关键注解 @Repository publicclassDeptDaoImpl1implementsDeptDao{} @Repository @Primary publicclassDeptDaoImpl2implementsDeptDao{} @Service publicclassDeptServiceImplimplementsDeptService{ @Autowired @Qulif......
  • MySQL 日志系统
    MySQL日志系统:一条SQL更新语句是如何执行的WAL:先写日志,再写磁盘(顺序写代替随机写,提高性能)两阶段提交:保证redolog和binlog一致性MySQL三种日志SQL更新语句和SQL查询语句一样要经过各功能模块的处理,区别是更新语句设计写日志(binlog、redolog、undolog)。binlog记录......
  • day11-特殊文件、日志技术、多线程
    day11-特殊文件、日志技术、多线程一、属性文件1.1特殊文件概述同学们,前面我们学习了IO流,我们知道IO流是用来读、写文件中的数据。但是我们接触到的文件都是普通的文本文件,普通的文本文件里面的数据是没有任何格式规范的,用户可以随意编写,如下图所示。像这种普通的文本文件,没......
  • lnav: 用于 Linux 的高级日志文件浏览器
    原创咬到舌头的小蛇IT开DD那点小事如果你想调试或排除问题,使用像lnav这样的高级日志文件查看器是非常必要的。它在任何Linux系统的终端中都能发挥巨大的作用。lnav:日志文件查看器lnav可以即时解压缩所有的压缩日志文件,并将它们合并在一起进行漂亮的显示。显示是根据......
  • iLogtail 开源两周年:UC 工程师分享日志查询服务建设实践案例
    作者:UC浏览器后端工程师,梁若羽传统ELK方案众所周知,ELK中的E指的是ElasticSearch,L指的是Logstash,K指的是Kibana。Logstash是功能强大的数据处理管道,提供了复杂的数据转换、过滤和丰富的数据输入输出支持。Filebeat是师出同门的轻量级日志文件收集器,在处理大量日志文......
  • NETCORE - 日志插件 Microsoft.Extensions.Logging
    NETCORE-日志插件Microsoft.Extensions.Loggingnetcore的默认日志插件为 Microsoft.Extensions.Logging,已集成在框架中。使用样例:namespaceRailGraph.Controllers{[ApiController][Route("[controller]")]publicclassANeo4jController:ControllerBas......
  • 【命令操作】查看和分析系统各类日志--journalctl
    原文链接:【命令操作】查看和分析系统各类日志–journalctl|统信|麒麟|方德Hello,大家好啊!今天给大家带来一篇关于Linux系统上journalctl命令详解的文章。journalctl是systemd的日志查看工具,用于查看和管理系统日志,包括内核消息、服务日志、用户日志等。通过journalctl......
  • Spring 过滤器 拦截器 监听器 Aop
    目录Spring过滤器拦截器监听器Aop1.过滤器2.拦截器3.监听器4.Aop5.参考文档Spring过滤器拦截器监听器Aop1.过滤器1.简介 过滤器Filter用于对数据进行过滤和预处理 过滤器只能在请求前后使用 依赖于servlet容器基于函数回调实现其生命周期由servlet容器管......