基于mybatis拦截器和数据库配置记录操作日志

1. 背景

应甲方的要求,系统中涉及到一些核心业务操作需要记录操作日志。

日志格式示例如下:

操作类型 操作内容 操作人员
更新 表单ID【669306331893190656】 姓名 由【张三】改成【李四】 年龄 由【17岁】改成【18岁】 性别 由【男】改成【女】 地址 由【地球上的某个部落】改成【火星上的某个部落】 更新时间【2024-12-20 18:28:58】 管理员-飞哥
新增 表单ID【669306331893190656】 姓名 由【】改成【张三】 年龄 由【】改成【17岁】 性别 由【】改成【男】 地址 由【】改成【地球上的某个部落】 创建时间【2024-12-20 08:08:08】 管理员-云哥

为了可以提高整体开发效率,不在记录日志这块浪费太多时间,最好是只需通过配置或者加注解的方式就可以便捷记录操作日志。

在 Mybatis 中,插件机制提供了非常强大的扩展能力,在 sql 最终执行之前,提供了四个拦截点,支持不同场景的功能扩展

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这里简单介绍这四种拦截点的区别:

  • Executor:代表执行器,由它调度 StatementHandler、ParameterHandler、ResultSetHandler 等来执行对应的 SQL,其中 StatementHandler 是最重要的。
  • StatementHandler:作用是使用数据库的 Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
  • ParameterHandler:是用来处理 SQL 参数的。
  • ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。

借用网上的一张 mybatis 执行过程来辅助说明

原文 blog.csdn.net/weixin_3949...

切点的选择,主要与它的功能强相关,这里主要是通过拦截mapper类相关方法,选择对Executor进行拦截。此外,操作日志的记录还涉及到一些字段名称、字典值的转换等等,因此还需要增加一张日志映射表,大致的思路是在拦截器里面获取当前操作表更新后的新数据,并调用特定方法获取操作前的旧数据进行比对,再根据日志映射表配置,对实体对象中有修改过的字段进行转换,按指定格式组转出日志内容。

以下是方案的具体实现:

2. 日志映射表

2.1 表结构

字段名 类型 注释
id bidint 主键,自增
module varchar 日志模块
field varchar 字段
field_name varchar 字段显示名称
convert_type char 转换类型,目前支持的类型有:00无需转换,01json,02sql,03英文逗号分隔,04日期
convert_resource text 转换资源
log_format char 日志格式
ord int 序号,数字越小优先级越高

2.2 字段简介

module: 日志模块,注解LogEnabled需要用到

field: 表字段,支持下划线、驼峰格式

fieldName: 字段名称

convert_type: 字段转换类型,目前支持以下5种类型

  • 00 :无需转换,原样展示
  • 01 :JSON,配置示例:
    [{"code":"01","value":"Individual"},{"code":"02","value":"Company"}]
    json的key固定必须有code和value,系统会用实体对象的具体字段和json的code进行比较,相同则取出对应value
  • 02 :SQL,SQL示例:
    select id as code,name as value from link_ep_car_type t where t.id = :originValue
    参数名固定是originValue,匹配规则类似01
  • 03 :英文逗号隔开,示例:
    [{"code":"00","value":"小米"},{"code":"01","value":"腾讯"},{"code":"02","value":"字节跳动"},{"code":"03","value":"阿里巴巴"},{"code":"04","value":"其他"}]
    json的key固定必须有code和value。例如字段值=01,02,会根据配置的json array匹配映射得到 腾讯,字节跳动
  • 04:日期格式,针对日期字段可指定日期格式化方式。没有指定的情况下,如果实体字段是date类型,格式化方式:yyyy-MM-dd HH:mm:ss。如果是LocalDate类型,格式化方式:yyyy-MM-dd,如果是LocalTime类型,格式化方式:HH:mm:ss

log_format: 日志格式

00:%s 由 【%s】改成【%s】,共用3个占位符,第一个占位符会用字段名替换,第二个占位符会用旧值替换,第三个占位符会用新值替换,效果示例:住址 由【福建省厦门市】 改成 【福建省莆田市】

10:%s 【%s】,共用2个占位符,第一个占位符会用字段名替换,第二个占位符会用新值替换,效果示例:表单ID 【203929302】

ord: 序号,值越小优先级越高,越先展示

3.注解

less 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited//可继承
public @interface LogEnabled {

    /**
     * 事件ID对应的字段
     * 最好是主键
     *
     * @return
     */
    String eventIdFiled();

    /**
     * 记录日志模块
     * 对应link_log_operation_reflect.module
     *
     * @return
     */
    String module();

    /**
     * 用来查询旧数据的自定义mapper方法名
     * 参数需是map类型,并且需通过paramFields声明用来获取旧数据的字段
     * <p>
     * 如果没有指定将默认使用selectByPrimaryKey方法获取
     *
     * @return
     */
    String selectOldEntityMapperMethod() default "";

    /**
     * 用来获取旧数据的字段
     * 如果有指定查询旧数据的自定义mapper方法名则需要声明该参数
     *
     * @return
     */
    String[] paramFields() default {};

    /**
     * 是否保存到日志表
     * 默认保存
     *
     * @return
     */
    boolean persistent() default true;
}

4.Mybatis拦截器代码

ini 复制代码
package mcr.interceptor;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import mcr.ApplicationContextHolder;
import mcr.annotation.LogEnabled;
import mcr.constants.LogConstants;
import mcr.service.log.LinkLogOperationRecordService;
import mcr.utils.ReflectionUtils;
import org.apache.ibatis.executor.Executor;
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.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
import org.apache.ibatis.session.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author: 飞哥
 * @desc: 操作日志拦截器
 * @date: 2023/8/7
 */
@Intercepts({
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class}
        )
})
public class OperationLogInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(OperationLogInterceptor.class);

    /**
     * 日志记录逻辑说明
     * <p>
     * 需要记录操作日志的实体需加注解LogEnabled,声明对应的module,以及eventIdFiled
     * LogEnabled注解说明:
     * (1)module:日志模块,会据此从link_log_operation_reflect读取日志字段映射配置,目前支持字段转换的方式有json、SQL
     * json固定格式,示例:[{"code":"01","value":"個人"},{"code":"02","value":"公司"}]
     * sql查询字段需含code/value字段,查询条件用:originValue,示例: select id as `code`,name as `value` from `link_ep_car_type` t where t.id = :originValue
     * <p>
     * (2)paramFields:用来获取老数据的字段,默认会执行 mapper中名叫 selectOldEntity的方法获取老数据,注意此方法入参类型是Map<String, Object>
     * (3)eventIdFiled:用来记录事件ID的字段,最好是实体对象的主键
     * <p>
     * update操作会比对旧数据和新数据,数据一致或新数据为null的不记录
     * insert操作只记录新数据
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        OperationLog operationLog = OperationLog.empty();
        try {
            Object[] args = invocation.getArgs();

            //获取绑定的参数对象
            Object parameterObject = args[1];

            if (parameterObject == null) {
                return invocation.proceed();
            }

            //获取日志注解
            LogEnabled logEnabled = null;
            if (parameterObject instanceof HashMap) {
                //批量insert操作
                List<LogEnabled> annotationList = (List<LogEnabled>) ((HashMap) parameterObject).values().stream().map(list -> {
                    if (!(list instanceof List)) {
                        return null;
                    }
                    return ((List) list).stream().filter(obj -> {
                        return obj.getClass().getAnnotation(LogEnabled.class) != null;
                    }).map(obj -> obj.getClass().getAnnotation(LogEnabled.class)).findFirst().orElse(null);
                }).collect(Collectors.toList());
                logEnabled = CollectionUtils.isEmpty(annotationList) ? null : annotationList.get(0);
            } else {
                logEnabled = parameterObject.getClass().getAnnotation(LogEnabled.class);
            }

            if (logEnabled == null) {
                return invocation.proceed();
            }

            MappedStatement mappedStatement = (MappedStatement) args[0];
            SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();

            LogConstants.OPTION option;
            if (SqlCommandType.INSERT.equals(sqlCommandType)) {
                option = LogConstants.OPTION.ADD;
            } else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
                option = LogConstants.OPTION.UPDATE;
            } else {
//                option = LogConstants.OPTION.DELETE;
                return invocation.proceed();
            }

            //获取SQL语句和参数信息封装对象
            BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);

            //获取参数映射列表
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

            Configuration configuration = mappedStatement.getConfiguration();
            MetaObject metaObject = configuration.newMetaObject(parameterObject);

            //新的实体类
            List<Object> newEntityList = parameterObject instanceof HashMap ? (List<Object>) ((HashMap) parameterObject).get("list") : new ArrayList<Object>() {{
                add(parameterObject);
            }};

            //旧的实体类
            List<Object> oldEntityList = this.getOldEntityList(option, logEnabled, invocation, metaObject, mappedStatement, parameterMappings, parameterObject);
            operationLog = ApplicationContextHolder.getBean(LinkLogOperationRecordService.class).operationLogRecord(logEnabled.module(), option, logEnabled.persistent(), newEntityList, oldEntityList, logEnabled.eventIdFiled());
        } catch (Exception e) {
            log.error("记录操作日志拦截器出现异常:", e);
        }

        Object result = invocation.proceed();
        ApplicationContextHolder.getBean(LinkLogOperationRecordService.class).storeLogReportAndSaveLog(result, operationLog);
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 获取mapper名字
     *
     * @param mappedStatementId 映射语句ID
     * @return
     */
    private String extractMapperName(String mappedStatementId) {
        // 从 mappedStatementId 中提取出 Mapper 的名称
        int lastIndex = mappedStatementId.lastIndexOf(".");
        return mappedStatementId.substring(0, lastIndex);
    }

    /**
     * 获取根据主键查询DO对象的MappedStatement
     *
     * @param configuration     配置对象
     * @param mappedStatementId 映射语句ID
     * @return
     */
    private MappedStatement getSelectByPrimaryKeyMappedStatement(Configuration configuration, String mappedStatementId) {
        String selectByPrimaryKeyMapperId = mappedStatementId.replace(mappedStatementId.substring(mappedStatementId.lastIndexOf(CommonConstants.DOT) + 1), "selectByPrimaryKey");
        return configuration.getMappedStatement(selectByPrimaryKeyMapperId);
    }

    /**
     * 更新和删除场景获取旧数据
     *
     * @param option            操作日志类型
     * @param logEnabled        操作日志注解
     * @param invocation        调用对象
     * @param metaObject        当前提交的参数封装对象
     * @param mappedStatement   映射语句
     * @param parameterMappings 当前提交参数映射列表
     * @param parameterObject   参数对象
     * @return
     */
    private List<Object> getOldEntityList(LogConstants.OPTION option, LogEnabled logEnabled, Invocation invocation, MetaObject metaObject, MappedStatement mappedStatement, List<ParameterMapping> parameterMappings, Object parameterObject) {
        if (LogConstants.OPTION.UPDATE != option && LogConstants.OPTION.DELETE != option) {
            return Lists.newArrayList();
        }
        if (!StringUtils.hasText(logEnabled.selectOldEntityMapperMethod())) {
            return this.getOldEntityList(invocation, mappedStatement, parameterObject);
        }
        return this.getOldEntityList(metaObject, mappedStatement, parameterMappings, logEnabled.selectOldEntityMapperMethod(), logEnabled.paramFields());
    }

    /**
     * 默认用selectByPrimaryKeyMappedStatement获取旧数据
     *
     * @param invocation      拦截器调用对象
     * @param mappedStatement 映射语句
     * @param parameterObject 当前提交参数映射列表
     * @return
     */
    private List<Object> getOldEntityList(Invocation invocation, MappedStatement mappedStatement, Object parameterObject) {
        Executor executor = (Executor) invocation.getTarget();
        MappedStatement selectByPrimaryKeyMappedStatement = this.getSelectByPrimaryKeyMappedStatement(mappedStatement.getConfiguration(), mappedStatement.getId());
        List<Object> oldEntityList = Lists.newArrayList();
        try {
            //单个实体操作
            List<Object> resultList = executor.query(selectByPrimaryKeyMappedStatement, parameterObject, RowBounds.DEFAULT, null);
            oldEntityList.addAll(resultList);
        } catch (Exception e) {
            log.error("获取旧实体对象出现异常:", e);
        }
        return oldEntityList;
    }

    /**
     * 用当前提交的实体对象中的部分字段查询更新之前的旧数据
     * 实体对应的MAPPER需有selectOldEntity方法
     *
     * @param metaObject                  当前提交的参数封装对象
     * @param mappedStatement             映射语句
     * @param parameterMappings           当前提交参数映射列表
     * @param selectOldEntityMapperMethod 用来查询旧数据的mapper方法名
     * @param paramFields                 用来获取旧数据的字段
     * @return
     */
    private List<Object> getOldEntityList(MetaObject metaObject, MappedStatement mappedStatement, List<ParameterMapping> parameterMappings, String selectOldEntityMapperMethod, String[] paramFields) {
        List<Object> oldEntityList = Lists.newArrayList();
        try {
            if (metaObject.getOriginalObject() instanceof HashMap) {
                //批量操作
                ArrayList objectList = ((ArrayList) metaObject.getValue("list"));
                for (int index = 0; index < objectList.size(); index++) {
                    Object object = objectList.get(index);
                    Class clz = Class.forName(this.extractMapperName(mappedStatement.getId()));
                    Method method = clz.getMethod(selectOldEntityMapperMethod, Map.class);
                    Object bean = ApplicationContextHolder.getBean(clz);
                    oldEntityList.add(method.invoke(bean, this.fillParams(object, index, parameterMappings, paramFields)));
                }
            } else {
                //单个实体操作
                Class clz = Class.forName(this.extractMapperName(mappedStatement.getId()));
                Method method = clz.getMethod(selectOldEntityMapperMethod, Map.class);
                Object bean = ApplicationContextHolder.getBean(clz);
                oldEntityList.add(method.invoke(bean, this.fillParams(metaObject, parameterMappings, paramFields)));
            }

        } catch (Exception e) {
            log.error("获取旧实体对象出现异常:{}", mappedStatement.getId(), e);
        }
        return oldEntityList;
    }

    /**
     * 填充查询参数
     *
     * @param metaObject        当前提交的参数封装对象
     * @param parameterMappings 当前提交参数映射列表
     * @param paramFields       用来获取旧数据的字段
     * @return
     */
    private Map<String, Object> fillParams(MetaObject metaObject, List<ParameterMapping> parameterMappings, String[] paramFields) {
        List<String> list = Lists.newArrayList(paramFields);
        Map<String, Object> params = Maps.newHashMap();
        parameterMappings.forEach(p -> {
            String key = p.getProperty();
            if (list.contains(key)) {
                Object value = metaObject.getValue(key);
                params.put(key, value);
            }
        });
        return params;
    }

    /**
     * 填充查询参数
     * 适用于批量操作场景
     *
     * @param object            当前提交的参数封装对象
     * @param index             批量操作参数索引
     * @param parameterMappings 当前提交参数映射列表
     * @param paramFields       用来获取旧数据的字段
     * @return
     */
    private Map<String, Object> fillParams(Object object, int index, List<ParameterMapping> parameterMappings, String[] paramFields) {
        List<String> list = Lists.newArrayList(paramFields);
        Map<String, Object> params = Maps.newHashMap();
        parameterMappings.forEach(p -> {
            try {
                //批量操作的时候property格式是__frch_record_0.id
                String key = p.getProperty().replaceAll(ForEachSqlNode.ITEM_PREFIX + "(.*)_" + index + "\.", "");
                if (list.contains(key)) {
                    Field field = ReflectionUtils.getAccessibleField(object, ApplicationContextHolder.getBean(LinkLogOperationRecordService.class).getCamelCaseString(key));
                    Object value = field.get(object);
                    params.put(key, value);
                }
            } catch (Exception e) {
                log.error("填充查询参数出现异常:{}", p, e);
            }
        });
        return params;
    }
}

5.实体对象字段转换代码

kotlin 复制代码
/**
 * @author: 飞哥
 * @desc:
 * @date: 2023/8/8
 */
@RefreshScope
@Service
public class LinkLogOperationRecordService extends BaseService<LinkLogOperationRecordMapper, LinkLogOperationRecordDO> {

    private static final Logger log = LoggerFactory.getLogger(LinkLogOperationRecordService.class);

    @Autowired
    private I18nMessageSource i18nMessageSource;
    @Autowired
    private LinkLogOperationReflectService logOperationReflectService;

    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    public LinkLogOperationRecordService(@Qualifier("defaultDataSource") DruidDataSource druidDataSource) {
        this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(druidDataSource);
    }

    /**
     * 保存日志
     *
     * @param recordId
     * @param eventType
     * @param content
     */
    private void saveLog(String recordId, String eventType, String content) {
        try {
            LinkLogOperationRecordDO record = new LinkLogOperationRecordDO();
            record.setEventId(recordId);
            record.setEventType(eventType);
            record.setContent(content);

            UserDTO user = WebUtils.getUser();

            record.setOperatorBy(user == null ? CommonConstants.SYSTEM : user.getUserId());
            record.setOperatorByName(user == null ? CommonConstants.SYSTEM_NAME : user.getUserName());
            this.save(record);
        } catch (Exception e) {
            log.error("日志入库失败:{} {}", recordId, eventType, e);
        }
    }

    /**
     * 暂存即将上报的操作日志
     *
     * @param recordId
     * @param content
     */
    private void storeLogReport(String recordId, String content) {
        try {
            LogReportInfo logReportInfo = new LogReportInfo();
            logReportInfo.setRecordId(recordId);
            logReportInfo.setContent(content);
            logReportInfo.setOperateTime(new Date());
            WebUtils.setLogReport(logReportInfo);
        } catch (Exception e) {
            log.error("日志暂存失败:{}", recordId, e);
        }
    }

    /**
     * 暂存即将上报的操作日志+日志入库
     *
     * @param invocationResult 更新记录数 >0才记录日志
     * @param operationLog     操作日志对象
     */
    public void storeLogReportAndSaveLog(Object invocationResult, OperationLog operationLog) {
        try {
            if (!operationLog.isEmpty() && Integer.parseInt(invocationResult.toString()) > 0) {
                this.storeLogReportAndSaveLog(operationLog.getRecordId(), operationLog.getOption().getCode(), operationLog.getContent(), operationLog.getPersistent());
            } else {
                log.warn("没有日志内容或没有更新记录,不记录操作日志:{} {}", invocationResult, JSON.toJSONString(operationLog));
            }
        } catch (Exception e) {
            log.error("记录日志失败:", e);
        }
    }

    /**
     * 暂存即将上报的操作日志+日志入库
     *
     * @param recordId
     * @param eventType
     * @param content
     * @param persistent
     */
    public void storeLogReportAndSaveLog(String recordId, String eventType, String content, boolean persistent) {
        this.storeLogReport(recordId, content);
        if (persistent) {
            this.saveLog(recordId, eventType, content);
        }
    }

    /**
     * 查詢操作日志
     *
     * @param eventId
     * @return
     */
    public List<LinkLogOperationRecordDO> listAll(String eventId) {
        Example example = this.getExample();
        example.and().andEqualTo("eventId", eventId);
        example.setOrderByClause("id desc");
        return this.selectByExample(example);
    }

    /**
     * 记录操作日志
     *
     * @param module        慕课
     * @param option        操作类型
     * @param persistent    是否持久化到数据库
     * @param newEntityList 准备更新的数据对象集合
     * @param oldEntityList 历史数据对象集合
     * @param eventIdField  实体对象中作为eventId的字段
     * @param <T>
     */
    public <T> OperationLog operationLogRecord(String module, LogConstants.OPTION option, boolean persistent, List<T> newEntityList, List<T> oldEntityList, String eventIdField) {
        try {
            List<LinkLogOperationReflectDO> reflectList = this.logOperationReflectService.listLogOperationReflect(module);
            for (int i = 0; i < newEntityList.size(); i++) {
                List<String> contentList = Lists.newArrayList();
                T newEntity = newEntityList.get(i);
                T oldEntity = oldEntityList.size() > i ? oldEntityList.get(i) : null;
                reflectList.forEach(o -> {
                    try {
                        //字段
                        String column = o.getField();
                        //字段对应名称,默认翻译成英文
                        String name = o.getFieldName();
                        ValueInfo valueInfo = this.getValueInfo(column, option, newEntity, oldEntity);
                        if (valueInfo == null) {
                            return;
                        }
                        Object newValue = valueInfo.newValue;
                        Object oldValue = valueInfo.oldValue;
                        if (newValue == null) {
                            //更新后的属性值=null,忽略
                            return;
                        }
                        LogConstants.LOG_FORMAT logFormat = LogConstants.LOG_FORMAT.codeOf(o.getLogFormat());
                        if (LogConstants.LOG_FORMAT.MAJOR_KEY == logFormat) {
                            String newValueConvert = this.convertFieldValue(newValue, o.getConvertType(), o.getConvertResource());
                            contentList.add(String.format(logFormat.getFormat(), name, newValueConvert));
                        } else {
                            if (newValue.equals(oldValue)) {
                                //属性值不变
                                return;
                            }
                            String oldValueConvert = this.convertFieldValue(oldValue, o.getConvertType(), o.getConvertResource());
                            String newValueConvert = this.convertFieldValue(newValue, o.getConvertType(), o.getConvertResource());

                            contentList.add(String.format(logFormat.getFormat(), name, oldValueConvert, newValueConvert));
                        }
                    } catch (Exception e) {
                        log.error("组装操作日志内容出现异常:{} {} ", module, JSON.toJSONString(o), e);
                    }
                });

                Field field = ReflectionUtils.getAccessibleField(newEntity, this.getCamelCaseString(eventIdField));
                String fieldValue = Objects.toString(field.get(newEntity), "");
                log.info("日志内容:{}", contentList);
                if (CollectionUtils.isEmpty(contentList)) {
                    return OperationLog.empty();
                }
                String content = Joiner.on("\r").join(contentList);

                return OperationLog.Builder
                        .create()
                        .withRecordId(fieldValue)
                        .withPersistent(persistent)
                        .withOption(option)
                        .withContent(content)
                        .build();
            }
        } catch (Exception e) {
            log.error("记录操作日志出现异常:{},{},{}", module, option, eventIdField, e);
        }

        return OperationLog.empty();
    }

    /**
     * 记录操作日志
     *
     * @param module       慕课
     * @param option       操作类型
     * @param persistent   是否持久化到数据库
     * @param newEntity    准备更新的数据对象
     * @param oldEntity    历史数据对象
     * @param eventIdField 实体对象中作为eventId的字段
     * @param <T>
     */
    public <T> void operationLogRecord(String module, LogConstants.OPTION option, boolean persistent, T newEntity, T oldEntity, String eventIdField) {
        OperationLog operationLog = this.operationLogRecord(module, option, persistent, Arrays.asList(newEntity), Arrays.asList(oldEntity), eventIdField);
        if (!operationLog.isEmpty()) {
            this.storeLogReportAndSaveLog(operationLog.getRecordId(), operationLog.getOption().getCode(), operationLog.getContent(), operationLog.getPersistent());
        }
    }

    /**
     * 获取新旧value
     *
     * @param column
     * @param option
     * @param newEntity
     * @param oldEntity
     * @param <T>
     * @return
     */
    private <T> ValueInfo getValueInfo(String column, LogConstants.OPTION option, T newEntity, T oldEntity) {
        try {
            // 获取属性对象
            Field field = ReflectionUtils.getAccessibleField(newEntity, this.getCamelCaseString(column));
            //获取getter方法
            Method method = ReflectionUtils.getAccessibleMethod(newEntity, "get" + StringUtils.capitalize(this.getCamelCaseString(column)));
            if (field == null && method == null) {
                log.error("没有这个字段,也没有getter方法,直接跳过:{}", column);
                return null;
            }
            if (field != null) {
                // 设置访问权限(可选)
                field.setAccessible(true);
                // 读取属性值
                Object newValue = field.get(newEntity);
                Object oldValue = oldEntity == null ? null : (LogConstants.OPTION.UPDATE == option ? field.get(oldEntity) : null);
                return new ValueInfo(newValue, oldValue);
            } else {
                //读取getter方法
                Object newValue = method.invoke(newEntity);
                Object oldValue = oldEntity == null ? null : (LogConstants.OPTION.UPDATE == option ? method.invoke(oldEntity) : null);
                return new ValueInfo(newValue, oldValue);
            }
        } catch (Exception e) {
            log.error("获取新旧value出现异常:{}", column, e);
        }
        return null;
    }

    /**
     * 转换字段值
     * convertTypeString转换规则说明:
     * 默认 00,表示不需要转换,直接返回原始值
     * 01 JSON,表示convertResource是一个jsonArray,原始值会从convertResource中匹配,固定格式:[{code:x1, value:y1},{code:x2,value:y2}]
     * 02 SQL,表示convertResource是一个SQL,执行结果格式固定为[{code:x1, value:y1},{code:x2,value:y2}],SQL有一个固定查询参数,名字为originValue,SQL示例:select id as `code`,name as `value` from `link_district` t where t.id = :originValue;
     * 03 英文逗号分隔,主要针对存储值为01,02,03类型的字段,会先用英文逗号分隔,然后再从convertResource匹配,convertResource固定格式:[{code:x1, value:y1},{code:x2,value:y2}]
     * 04 日期,主要针对一些需要转换为特定格式的日期字段,例如可以填yyyy-MM-dd,当如果不特殊指定,也就是convertTypeString=00时也可以转换日期,只是格式默认是yyyy-MM-dd HH:mm:ss
     *
     * @param originValue       原始数据
     * @param convertTypeString 转换类型
     * @param convertResource   转换资源,JSON、SQL
     * @return
     */
    public String convertFieldValue(Object originValue, String convertTypeString, String convertResource) {
        String originValueString = Objects.toString(originValue, "");
        List<ConvertInfo> list = Lists.newArrayList();
        LogConstants.CONVERT_TYPE translateType = LogConstants.CONVERT_TYPE.codeOf(convertTypeString);
        if (translateType != null) {
            switch (translateType) {
                case IGNORE:
                    return this.formatDate(originValue, originValueString, null);
                case JSON:
                    list = JSONArray.parseArray(convertResource, ConvertInfo.class);
                    break;
                case SQL:
                    list = this.namedParameterJdbcTemplate.query(convertResource, ImmutableMap.of("originValue", originValueString), new BeanPropertyRowMapper<>(ConvertInfo.class));
                    break;
                case SPLIT:
                    List<String> originValueList = Lists.newArrayList(originValueString.split(","));
                    List<ConvertInfo> convertInfoList = JSONArray.parseArray(convertResource, ConvertInfo.class);
                    return Joiner.on(",").join(originValueList.stream().map(value -> {
                        return convertInfoList.stream().filter(o -> value.equals(o.getCode())).findFirst().map(ConvertInfo::getValue).orElse(value);
                    }).collect(Collectors.toList()));
                case DATE:
                    return this.formatDate(originValue, originValueString, convertResource);
                
                default:
                    log.error("暂不支持的转换类型:{}", convertTypeString);
                    break;
            }
        } else {
            log.error("暂不支持的转换类型:{}", convertTypeString);
        }
        //找不到翻译则返回原始值
        return list.stream().filter(o -> originValueString.equals(o.getCode())).findFirst().map(ConvertInfo::getValue).orElse(originValueString);
    }

    /**
     * 格式化日期字段
     *
     * @param originValue
     * @param originValueString
     * @param format
     * @return
     */
    private String formatDate(Object originValue, String originValueString, String format) {
        if (originValue instanceof Date) {
            return DateUtil.convertDate2LocalDateTime((Date) originValue).format(!StringUtils.hasLength(format) ? DateUtil.DTF_YMDHMS_2 : DateTimeFormatter.ofPattern(format));
        }
        if (originValue instanceof LocalDate) {
            return ((LocalDate) originValue).format(!StringUtils.hasLength(format) ? DateUtil.DTF_YYYYMMDD_2 : DateTimeFormatter.ofPattern(format));
        }
        if (originValue instanceof LocalTime) {
            return ((LocalTime) originValue).format(!StringUtils.hasLength(format) ? DateUtil.DTF_HHMMSS : DateTimeFormatter.ofPattern(format));
        }
        if (originValue instanceof Long) {
            LocalDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli((Long) originValue), ZoneId.systemDefault()).toLocalDateTime();
            return dateTime.format(!StringUtils.hasLength(format) ? DateUtil.DTF_YMDHMS_2 : DateTimeFormatter.ofPattern(format));
        }
        return originValueString;
    }

    /**
     * 下划线字段转为驼峰
     * 例如back_end会转换成backEnd
     *
     * @param underScoreField
     * @return
     */
    public String getCamelCaseString(String underScoreField) {
        return underScoreField.contains(CommonConstants.UNDERLINE) ? CaseUtils.toCamelCase(underScoreField, Boolean.FALSE, '_') : underScoreField;
    }

    public class ValueInfo {
        private Object newValue;
        private Object oldValue;

        public ValueInfo(Object newValue, Object oldValue) {
            this.newValue = newValue;
            this.oldValue = oldValue;
        }

        public Object getNewValue() {
            return newValue;
        }

        public void setNewValue(Object newValue) {
            this.newValue = newValue;
        }

        public Object getOldValue() {
            return oldValue;
        }

        public void setOldValue(Object oldValue) {
            this.oldValue = oldValue;
        }
    }
}

6.添加Mybatis拦截器

ini 复制代码
@Configuration
public class DbConfig {
    @Bean
    public SqlSessionFactory buildSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        // 创建 ResourcePatternResolver 对象
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 使用 ResourcePatternResolver 获取 Mapper 文件对应的 Resource 对象
        Resource[] resources = resolver.getResources(mapperLocations);
        sessionFactoryBean.setMapperLocations(resources);
        //Properties properties = new Properties();
        SqlSessionFactory sqlSessionFactory = sessionFactoryBean.getObject();

        org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
        configuration.setMapUnderscoreToCamelCase(true);

        //记录操作日志
        configuration.addInterceptor(new OperationLogInterceptor());

        return sqlSessionFactory;
    }
}

7.业务实践

至此,底层基础工作已经完成,接下来后续需要记录操作日志的表对应DO只需要加上刚才声明的注解,并在表里配置字段映射即可自动记录操作日志,如下所示:

arduino 复制代码
@LogEnabled(eventIdFiled = "userId", module = "user_info")
@Table(name = "user_info")
public class UserInfoDO {

    /**
     * 主鍵
     */
    @Id
    private String userId;

    /**
     * 姓名
     */
    private String name;

    /**
     * 性别
     */
    private String sex;
    
    /**
     * 年龄
     */
    private Integer age;

    /**
     * 地址
     */
    private String address;
    
    /**
     * 创建时间
     */
    private Date createTime;
    
    /**
     * 更新时间
     */
    private Date updateTime;

    ...getter/setter
}

映射表配置示例:

module field field_name convert_type convert_source log_format ord
user_info userId 用户ID 00 10 10
user_info name 姓名 00 00 20
user_info sex 性别 01 [{"code":"01","value":"男"},{"code":"02","value":"女"}] 00 30
user_info age 年龄 02 select :originValue as code, concat(:originValue,'岁') 00 40
user_info address 地址 00 00 50
user_info createTime 创建时间 04 00 60
user_info updateTime 更新时间 04 00 70

这样后续对UserInfoDO所在mapper进行insert或者update就会自动生成开头一样的操作日志。

贴出来的代码可能不是很全,但大致的思路可以供大家参考

相关推荐
GJCTYU1 小时前
spring中@Transactional注解和事务的实战理解附代码
数据库·spring boot·后端·spring·oracle·mybatis
DuelCode12 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
荔枝吻14 小时前
【沉浸式解决问题】idea开发中mapper类中突然找不到对应实体类
java·intellij-idea·mybatis
JAVA学习通14 小时前
Mybatis--动态SQL
sql·tomcat·mybatis
新world1 天前
mybatis-plus从入门到入土(二):单元测试
单元测试·log4j·mybatis
RainbowSea1 天前
问题 1:MyBatis-plus-3.5.9 的分页功能修复
java·spring boot·mybatis
JAVA学习通1 天前
Mybatis----留言板
mybatis
双力臂4042 天前
MyBatis动态SQL进阶:复杂查询与性能优化实战
java·sql·性能优化·mybatis
慕y2742 天前
Java学习第十五部分——MyBatis
java·学习·mybatis
一勺菠萝丶2 天前
Spring Boot + MyBatis/MyBatis Plus:XML中循环处理List参数的终极指南
xml·spring boot·mybatis