基于Mybatis-Plus的数据库操作日志方案

摘要:本文主要介绍了如何使用Mybatis-Plus数据变动记录插件 来记录我们的业务操作日志,下文主要改造了DataChangeRecorderInnerInterceptor拦截器,插入了我们业务实际的一些操作,可以让我们更加方便的使用,项目采用SpringBoot + Mybatis-Plus

简介

官方的DataChangeRecorderInnerInterceptor插件会拦截每一条insert/update/delete语句,而我们的业务中,我们只需要关注我们需要拦截的业务,所以在该插件的基础上定制了实际的业务场景代码,并且输出的变更语句对象更加容易后期扩展。

  • 只针对单表的insert/update/delete拦截,复杂sql不支持。
  • update/delete操作如果主键不存在,可能会导致性能损耗严重。
  • 批量插入、更新数据不支持,需要改为for循环执行。

详细代码

测试数据库表

定义了两张表用于测试业务

sql 复制代码
CREATE TABLE `userinfo` (
  `id` bigint(20) NOT NULL,
  `code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `money` decimal(15,4) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `sub` (
  `id` bigint(20) NOT NULL,
  `parent_id` bigint(11) DEFAULT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `num` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

操作日志数据库表

如果是单系统,可以放在一起,但是最好通过MQ解耦,操作日志单独在一个系统中,本案例是放在一起的,用于简单演示。

sql 复制代码
CREATE TABLE `operate_log` (
  `id` bigint(20) NOT NULL,
  `trace_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '业务追踪ID',
  `domain` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '归属领域',
  `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作类型',
  `table_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作表名',
  `table_desc` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作表描述',
  `data_id` bigint(20) DEFAULT NULL COMMENT '数据id',
  `db_operation` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库操作类型',
  `cost` bigint(20) DEFAULT NULL COMMENT '本次拦截时间消耗(ms)',
  `generate_time` datetime(3) DEFAULT NULL COMMENT '拦截的数据生成时间',
  `create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志';

CREATE TABLE `operate_log_item` (
  `id` bigint(20) NOT NULL,
  `operate_log_id` bigint(20) DEFAULT NULL COMMENT '操作日志ID',
  `column_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '列名称',
  `column_desc` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '列描述',
  `original_value` text COLLATE utf8mb4_unicode_ci COMMENT '原始值',
  `update_value` text COLLATE utf8mb4_unicode_ci COMMENT '更新后的值',
  PRIMARY KEY (`id`),
  KEY `idx_oli` (`operate_log_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

基础Entity/Mapper相关

UserinfoEntity

less 复制代码
@ApiModel(value = "用户信息")
@Data
@TableName("userinfo")
public class UserinfoEntity {

    @ApiModelProperty(value = "id")
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    @ApiModelProperty(value = "编码")
    private String code;

    @ApiModelProperty(value = "名称")
    private String name;

    private BigDecimal money;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

}

SubEntity

kotlin 复制代码
@Data
@TableName("sub")
public class SubEntity {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    @ApiModelProperty(value = "父id")
    private Long parentId;

    @ApiModelProperty(value = "名称")
    private String name;

    @ApiModelProperty(value = "数量")
    private Integer num;
}

OperateLogEntity

less 复制代码
@OperateLogIgnore
@ApiModel(value = "操作日志表")
@Data
@TableName("operate_log")
public class OperateLogEntity {

    /** 数据ID */
    private Long id;
    /** 追踪ID */
    private String traceId;
    /** 归属领域 */
    private String domain;
    /** 操作类型 */
    private String type;
    /** 表名称 */
    private String tableName;
    /** 表描述 */
    private String tableDesc;
    /** 数据ID */
    private String dataId;
    /** 数据库操作类型 */
    private String dbOperation;
    /** 花费时间毫秒 */
    private Long cost;
    /** 数据生成的时间 */
    private Date generateTime;
    /** 创建时间 */
    private Date createTime;

}

OperateLogItemEntity

less 复制代码
@OperateLogIgnore
@Data
@TableName("operate_log_item")
public class OperateLogItemEntity {

    private Long id;

    private Long operateLogId;

    /** 数据库列字段 */
    private String columnName;
    /** 字段描述 */
    private String columnDesc;
    /** 原始值 */
    private String originalValue;
    /** 更新后的值 */
    private String updateValue;

}

UserinfoMapper

less 复制代码
@Repository
public interface UserinfoMapper extends BaseMapper<UserinfoEntity> {

    @Update("update userinfo set code = #{code} where id = #{id}")
    void complexUpdate(@Param("code") String code,@Param("id")  Long id);

    @Update("update userinfo,sub set userinfo.money = sub.num where userinfo.id = sub.parent_id and userinfo.id = #{id}")
    void complexUpdate2(@Param("id")  Long id);
}

SubMapper

java 复制代码
@Repository
public interface SubMapper extends BaseMapper<SubEntity> {
}

OperateLogMapper

java 复制代码
@Repository
public interface OperateLogMapper extends BaseMapper<OperateLogEntity> {
}

OperateLogItemMapper

java 复制代码
@Repository
public interface OperateLogItemMapper extends BaseMapper<OperateLogItemEntity> {
}

定义注解

OperateLog操作日志注解

只有Controller上的方法加了该注解才记录后面相关的操作日志。

less 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {

    /**
     * 操作类型
     * Alias for the type();
     * @return
     */
    String value();

    /**
     * 业务领域
     * @return
     */
    String domain() default "";

    /**
     * 操作类型
     * @return
     */
    String type() default "";
}

OperateLogIgnore忽略记录的表注解

加了该注解后,就不会存储该表数据变动的日志。

less 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLogIgnore {
}

dto定义

用于该业务上下文的dto

OperateLogContextDto 业务上下文DTO

dto配合@OperateLog注解使用的,有了该注解,上下文才会存储这个对象。

arduino 复制代码
@Data
public class OperateLogContextDto {

    /** 调用链路ID */
    private String traceId;

    /** 归属领域 */
    private String domain;

    /** 操作类型 */
    private String type;

}

TableInfoDto 表结构信息

该类存储了我们定义的Entity对象的表描述和字段描述信息

  • 表名描述用@ApiModel(value = "用户信息")
  • 属性描述用@ApiModelProperty(value = "编码")

这样就能把实体对应的描述存储起来。

arduino 复制代码
@Data
public class TableInfoDto {

    /** 表名称 */
    private String tableName;
    /** 表描述 */
    private String tableDesc;
    /** 是否忽略表 */
    private Boolean ignore = Boolean.FALSE;

    /**
     * 属性map
     * key=表的属性名称  value=属性描述
     */
    private Map<String, String> columnMap = new LinkedHashMap<>();

}

数据变动相关DTO

DataChangeColumnResult列变动记录

记录了列变动的先后值

arduino 复制代码
@Data
public class DataChangeColumnResult {
    /** 数据库列字段 */
    private String columnName;
    /** 字段描述 */
    private String columnDesc;
    /** 原始值 */
    private String originalValue;
    /** 更新后的值 */
    private String updateValue;
}
DataChangeResult数据行变动记录

记录一行数据的变动情况

ruby 复制代码
@Data
public class DataChangeResult {
    /** id */
    private String id;
    /** 其他的列变动情况 */
    private List<DataChangeColumnResult> changeColumnResults;
}
OperateLogResult 每一次数据库操作影响的数据行和操作集合
arduino 复制代码
@Data
public class OperateLogResult {

    /** 上下文信息 */
    private OperateLogContextDto context;

    /** 数据库操作类型 */
    private String operation;
    /** 数据记录状态 */
    private boolean recordStatus;
    /** 操作的表名称 */
    private String tableName;
    /** 表描述 */
    private String tableDesc;
    /** 数据变更记录 */
    private List<DataChangeResult> dataChangeResults = new ArrayList<>();
    /**
     * cost for this plugin, ms
     */
    private long cost;
    /** 数据构建的时间 */
    private long generateTime = System.currentTimeMillis();

}

OperateLogContextHolder上下文信息持有者

controller的方法上有@OperateLog注解,则会通过拦截器放入操作信息到该上下文中。

csharp 复制代码
public class OperateLogContextHolder {

    private static final ThreadLocal<OperateLogContextDto> operateTraceDtoContext = new ThreadLocal<>();

    /**
     * 设置上下文
     * @param operateLogContextDto
     */
    public static void set(OperateLogContextDto operateLogContextDto){
        if(null != operateLogContextDto){
            operateTraceDtoContext.set(operateLogContextDto);
        }
    }

    /**
     * 获取上下文
     * @return
     */
    public static OperateLogContextDto get(){
        return operateTraceDtoContext.get();
    }

    /**
     * 清除上下文
     */
    public static void clear(){
        operateTraceDtoContext.remove();
    }

}

SpringEvent事件定义

本方案采用的是事件解耦

OperateLogTransactionEvent带事务的事件对象

scala 复制代码
@Data
public class OperateLogTransactionEvent extends ApplicationEvent {

    private static final long serialVersionUID = 4675816574446023168L;

    private OperateLogResult operateLogResult;

    public OperateLogTransactionEvent(Object source, OperateLogResult operateLogResult) {
        super(source);
        this.operateLogResult = operateLogResult;
    }
}

OperateLogNormalEvent普通事件对象

scala 复制代码
@Data
public class OperateLogNormalEvent extends ApplicationEvent {

    private static final long serialVersionUID = 2795119755419728981L;
    private OperateLogResult operateLogResult;

    public OperateLogNormalEvent(Object source, OperateLogResult operateLogResult) {
        super(source);
        this.operateLogResult = operateLogResult;
    }
}

事件监听者

用来消费事件消息,做操作日志最后的存储

less 复制代码
@Slf4j
@Component
public class OperateLogEventListener {

    @Autowired
    private OperateLogMapper operateLogMapper;
    @Autowired
    private OperateLogItemMapper operateLogItemMapper;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void transaction(OperateLogTransactionEvent operateLogTransactionEvent) {
        // 在这里编写你想在事务提交后执行的代码
        log.info(JSON.toJSONString(operateLogTransactionEvent));
        this.saveLog(operateLogTransactionEvent.getOperateLogResult());
    }

    @EventListener
    public void normal(OperateLogNormalEvent operateLogNormalEvent){
        // 在这里编写你想在事务提交后执行的代码
        log.info(JSON.toJSONString(operateLogNormalEvent));
        this.saveLog(operateLogNormalEvent.getOperateLogResult());
    }

    private void saveLog(OperateLogResult operateLogResult){
        for (DataChangeResult dataChangeResult : operateLogResult.getDataChangeResults()) {
            OperateLogEntity operateLogEntity = new OperateLogEntity();
            operateLogEntity.setId(IdWorker.getId());
            operateLogEntity.setTraceId(operateLogResult.getContext().getTraceId());
            operateLogEntity.setDomain(operateLogResult.getContext().getDomain());
            operateLogEntity.setType(operateLogResult.getContext().getType());
            operateLogEntity.setTableName(operateLogResult.getTableName());
            operateLogEntity.setTableDesc(operateLogResult.getTableDesc());
            operateLogEntity.setDbOperation(operateLogResult.getOperation());
            operateLogEntity.setCost(operateLogResult.getCost());
            operateLogEntity.setGenerateTime(new Date(operateLogResult.getGenerateTime()));
            operateLogEntity.setDataId(dataChangeResult.getId());
            operateLogEntity.setCreateTime(new Date());
            operateLogMapper.insert(operateLogEntity);
            for (DataChangeColumnResult changeColumnResult : dataChangeResult.getChangeColumnResults()) {
                OperateLogItemEntity operateLogItemEntity = new OperateLogItemEntity();
                operateLogItemEntity.setId(IdWorker.getId());
                operateLogItemEntity.setOperateLogId(operateLogEntity.getId());
                operateLogItemEntity.setColumnName(changeColumnResult.getColumnName());
                operateLogItemEntity.setColumnDesc(changeColumnResult.getColumnDesc());
                operateLogItemEntity.setOriginalValue(changeColumnResult.getOriginalValue());
                operateLogItemEntity.setUpdateValue(changeColumnResult.getUpdateValue());
                operateLogItemMapper.insert(operateLogItemEntity);
            }
        }
    }
}

mvc拦截器配置

OperateLogWebInterceptor

判断controller的方法上是否有@OperateLog注解,然后存入上下文中

java 复制代码
public class OperateLogWebInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof HandlerMethod){
            Method method = ((HandlerMethod) handler).getMethod();
            OperateLog operateLog = method.getAnnotation(OperateLog.class);
            String operateType = this.buildOperateType(operateLog);
            if(StrUtil.isNotBlank(operateType)){
                OperateLogContextDto operateLogContextDto = new OperateLogContextDto();
                operateLogContextDto.setTraceId(UUID.randomUUID().toString());
                operateLogContextDto.setDomain(operateLog.domain());
                operateLogContextDto.setType(operateType);
                OperateLogContextHolder.set(operateLogContextDto);
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        OperateLogContextHolder.clear();
    }

    /**
     * 构建操作类型
     * @param operateLog
     * @return
     */
    private String buildOperateType(OperateLog operateLog){
        if(null == operateLog){
            return null;
        }
        if(StrUtil.isNotBlank(operateLog.value())){
            return operateLog.value();
        }
        return operateLog.type();
    }
}

OperateLogMvcConfig

配置拦截器在SpringBoot中生效

typescript 复制代码
@Configuration
public class OperateLogMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new OperateLogWebInterceptor());
    }
}

Mybatis-Plus数据拦截相关

OperateLogTableInfoHelper

用来存储表的结构信息工具类

scss 复制代码
@Slf4j
public class OperateLogTableInfoHelper {

    private static final Map<String, TableInfoDto> TABLE_INFO_DTO_MAP = new ConcurrentHashMap<>();

    /**
     * 获取表结构信息
     * @param tableName
     * @return
     */
    public static TableInfoDto getTableInfoDto(String tableName){
        if(!TABLE_INFO_DTO_MAP.containsKey(tableName)){
            synchronized (tableName.intern()){
                initTableInfoDto(tableName);
            }
        }
        return TABLE_INFO_DTO_MAP.get(tableName);
    }

    /**
     * 初始化表信息
     * @param tableName
     */
    private static void initTableInfoDto(String tableName){
        if(TABLE_INFO_DTO_MAP.containsKey(tableName)){
            return;
        }
        TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
        if(null == tableInfo){
            log.error("数据库表不存在,tableName={}",tableName);
            return;
        }
        TableInfoDto tableInfoDto = new TableInfoDto();
        Map<String, String> columnMap = new LinkedHashMap<>();
        tableInfoDto.setTableName(tableName);
        tableInfoDto.setColumnMap(columnMap);
        ApiModel apiModel = tableInfo.getEntityType().getAnnotation(ApiModel.class);
        if(null != apiModel){
            tableInfoDto.setTableDesc(apiModel.value());
        }
        OperateLogIgnore operateLogIgnore = tableInfo.getEntityType().getAnnotation(OperateLogIgnore.class);
        if(null != operateLogIgnore){
            tableInfoDto.setIgnore(true);
        }
        for (TableFieldInfo tableFieldInfo : tableInfo.getFieldList()) {
            Field field = ReflectUtil.getField(tableInfo.getEntityType(), tableFieldInfo.getProperty());
            if(null == field){
                continue;
            }
            ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
            String propertyName = null;
            if(null != apiModelProperty){
                propertyName = apiModelProperty.value();
            }
            columnMap.put(tableFieldInfo.getColumn(), propertyName);
        }
        TABLE_INFO_DTO_MAP.put(tableName, tableInfoDto);
    }

}

OperateLogMybatisInnerInterceptor数据拦截插件

主题内容来源于DataChangeRecorderInnerInterceptor插件,我在该基础上加上了业务操作

scss 复制代码
public class OperateLogMybatisInnerInterceptor implements InnerInterceptor {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    @SuppressWarnings("unused")
    public static final String IGNORED_TABLE_COLUMN_PROPERTIES = "ignoredTableColumns";

    private final Map<String, Set<String>> ignoredTableColumns = new ConcurrentHashMap<>();
    private final Set<String> ignoreAllColumns = new HashSet<>();//全部表的这些字段名,INSERT/UPDATE都忽略,delete暂时保留
    //批量更新上限, 默认一次最多1000条
    private int BATCH_UPDATE_LIMIT = 1000;
    private boolean batchUpdateLimitationOpened = false;
    private final Map<String, Integer> BATCH_UPDATE_LIMIT_MAP = new ConcurrentHashMap<>();//表名->批量更新上限

    private ApplicationEventPublisher publisher;

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        if(null == OperateLogContextHolder.get()){
            return;
        }
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        final BoundSql boundSql = mpSh.boundSql();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            OperateLogResult operateLogResult;
            long startTs = System.currentTimeMillis();
            try {
                Statement statement = JsqlParserGlobal.parse(mpBs.sql());
                if (statement instanceof Insert) {
                    operateLogResult = processInsert((Insert) statement, mpSh.boundSql());
                } else if (statement instanceof Update) {
                    operateLogResult = processUpdate((Update) statement, ms, boundSql, connection);
                } else if (statement instanceof Delete) {
                    operateLogResult = processDelete((Delete) statement, ms, boundSql, connection);
                } else {
                    logger.info("other operation sql={}", mpBs.sql());
                    return;
                }
            } catch (Exception e) {
                if (e instanceof DataUpdateLimitationException) {
                    throw (DataUpdateLimitationException) e;
                }
                logger.error("Unexpected error for mappedStatement={}, sql={}", ms.getId(), mpBs.sql(), e);
                return;
            }
            long costThis = System.currentTimeMillis() - startTs;
            if (operateLogResult != null) {
                operateLogResult.setCost(costThis);
                dealOperationResult(operateLogResult);
            }
        }
    }

    /**
     * 判断哪些SQL需要处理
     * 默认INSERT/UPDATE/DELETE语句
     *
     * @param sql
     * @return
     */
    protected boolean allowProcess(String sql) {
        String sqlTrim = sql.trim().toUpperCase();
        return sqlTrim.startsWith("INSERT") || sqlTrim.startsWith("UPDATE") || sqlTrim.startsWith("DELETE");
    }

    /**
     * 处理数据更新结果,默认打印
     *
     * @param operateLogResult
     */
    protected void dealOperationResult(OperateLogResult operateLogResult) {
        if(null == operateLogResult){
            return;
        }
        this.fillOperationResult(operateLogResult);
        if (TransactionSynchronizationManager.isActualTransactionActive()){
            publisher.publishEvent(new OperateLogTransactionEvent(this, operateLogResult));
        }else{
            publisher.publishEvent(new OperateLogNormalEvent(this, operateLogResult));
        }
    }

    /**
     * 填充其他数据
     * @param operateLogResult
     */
    private void fillOperationResult(OperateLogResult operateLogResult){
        TableInfoDto tableInfoDto = OperateLogTableInfoHelper.getTableInfoDto(operateLogResult.getTableName());
        if(null == tableInfoDto){
            return;
        }
        operateLogResult.setTableDesc(tableInfoDto.getTableDesc());
        operateLogResult.getDataChangeResults().forEach(dataChangeResult -> {
            dataChangeResult.getChangeColumnResults().forEach(dataChangeColumnResult -> {
                dataChangeColumnResult.setColumnDesc(tableInfoDto.getColumnMap().get(dataChangeColumnResult.getColumnName().toLowerCase()));
                if(dataChangeColumnResult.getColumnName().equalsIgnoreCase("id") && ObjectUtils.isEmpty(dataChangeResult.getId())){
                    dataChangeResult.setId(dataChangeColumnResult.getUpdateValue());
                }
            });
            dataChangeResult.getChangeColumnResults().removeIf(v -> v.getColumnName().equalsIgnoreCase("id"));
        });
        operateLogResult.setContext(OperateLogContextHolder.get());
    }

    public OperateLogResult processInsert(Insert insertStmt, BoundSql boundSql) {
        String operation = SqlCommandType.INSERT.name().toLowerCase();
        Table table = insertStmt.getTable();
        String tableName = table.getName();
        if (checkOperateLogIgnore(tableName)) {
            return null;
        }
        Optional<OperateLogResult> optionalOperationResult = ignoredTableColumns(tableName, operation);
        if (optionalOperationResult.isPresent()) {
            return optionalOperationResult.get();
        }
        OperateLogResult result = new OperateLogResult();
        result.setOperation(operation);
        result.setTableName(tableName);
        result.setRecordStatus(true);
        Map<String, Object> updatedColumnDatas = getUpdatedColumnDatas(tableName, boundSql, insertStmt);
        result.setDataChangeResults(this.buildDataStr(compareAndGetUpdatedColumnDatas(result.getTableName(), null, updatedColumnDatas)));
        return result;
    }

    /**
     * 检查是否是忽略的表
     * @param tableName
     * @return
     */
    private boolean checkOperateLogIgnore(String tableName) {
        TableInfoDto tableInfoDto = OperateLogTableInfoHelper.getTableInfoDto(tableName);
        if(null == tableInfoDto || tableInfoDto.getIgnore()){
            return true;
        }
        return false;
    }

    public OperateLogResult processUpdate(Update updateStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        Expression where = updateStmt.getWhere();
        PlainSelect selectBody = new PlainSelect();
        Table table = updateStmt.getTable();
        String tableName = table.getName();
        if (checkOperateLogIgnore(tableName)) {
            return null;
        }
        String operation = SqlCommandType.UPDATE.name().toLowerCase();
        Optional<OperateLogResult> optionalOperationResult = ignoredTableColumns(tableName, operation);
        if (optionalOperationResult.isPresent()) {
            return optionalOperationResult.get();
        }
        selectBody.setFromItem(table);
        List<Column> updateColumns = new ArrayList<>();
        for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
            updateColumns.addAll(updateSet.getColumns());
        }
        Columns2SelectItemsResult buildColumns2SelectItems = buildColumns2SelectItems(tableName, updateColumns);
        selectBody.setSelectItems(buildColumns2SelectItems.getSelectItems());
        selectBody.setWhere(where);
        SelectItem<PlainSelect> plainSelectSelectItem = new SelectItem<>(selectBody);

        BoundSql boundSql4Select = new BoundSql(mappedStatement.getConfiguration(), plainSelectSelectItem.toString(),
                prepareParameterMapping4Select(boundSql.getParameterMappings(), updateStmt),
                boundSql.getParameterObject());
        PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
        Map<String, Object> additionalParameters = mpBoundSql.additionalParameters();
        if (additionalParameters != null && !additionalParameters.isEmpty()) {
            for (Map.Entry<String, Object> ety : additionalParameters.entrySet()) {
                boundSql4Select.setAdditionalParameter(ety.getKey(), ety.getValue());
            }
        }
        Map<String, Object> updatedColumnDatas = getUpdatedColumnDatas(tableName, boundSql, updateStmt);
        OriginalDataObj originalData = buildOriginalObjectData(updatedColumnDatas, selectBody, buildColumns2SelectItems.getPk(), mappedStatement, boundSql4Select, connection);
        OperateLogResult result = new OperateLogResult();
        result.setOperation(operation);
        result.setTableName(tableName);
        result.setRecordStatus(true);
        result.setDataChangeResults(this.buildDataStr(compareAndGetUpdatedColumnDatas(result.getTableName(), originalData, updatedColumnDatas)));
        return result;
    }

    private Optional<OperateLogResult> ignoredTableColumns(String table, String operation) {
        final Set<String> ignoredColumns = ignoredTableColumns.get(table.toUpperCase());
        if (ignoredColumns != null) {
            if (ignoredColumns.stream().anyMatch("*"::equals)) {
                OperateLogResult result = new OperateLogResult();
                result.setOperation(operation);
                result.setTableName(table + ":*");
                result.setRecordStatus(false);
                return Optional.of(result);
            }
        }
        return Optional.empty();
    }

    private TableInfo getTableInfoByTableName(String tableName) {
        for (TableInfo tableInfo : TableInfoHelper.getTableInfos()) {
            if (tableName.equalsIgnoreCase(tableInfo.getTableName())) {
                return tableInfo;
            }
        }
        return null;
    }

    /**
     * 将update SET部分的jdbc参数去除
     *
     * @param originalMappingList 这里只会包含JdbcParameter参数
     * @param updateStmt
     * @return
     */
    private List<ParameterMapping> prepareParameterMapping4Select(List<ParameterMapping> originalMappingList, Update updateStmt) {
        List<Expression> updateValueExpressions = new ArrayList<>();
        for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
            updateValueExpressions.addAll(updateSet.getValues());
        }
        int removeParamCount = 0;
        for (Expression expression : updateValueExpressions) {
            if (expression instanceof JdbcParameter) {
                ++removeParamCount;
            }
        }
        return originalMappingList.subList(removeParamCount, originalMappingList.size());
    }

    protected Map<String, Object> getUpdatedColumnDatas(String tableName, BoundSql updateSql, Statement statement) {
        Map<String, Object> columnNameValMap = new HashMap<>(updateSql.getParameterMappings().size());
        Map<Integer, String> columnSetIndexMap = new HashMap<>(updateSql.getParameterMappings().size());
        List<Column> selectItemsFromUpdateSql = new ArrayList<>();
        if (statement instanceof Update) {
            Update updateStmt = (Update) statement;
            int index = 0;
            for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
                selectItemsFromUpdateSql.addAll(updateSet.getColumns());
                final ExpressionList<Expression> updateList = (ExpressionList<Expression>) updateSet.getValues();
                for (int i = 0; i < updateList.size(); ++i) {
                    Expression updateExps = updateList.get(i);
                    if (!(updateExps instanceof JdbcParameter)) {
                        columnNameValMap.put(updateSet.getColumns().get(i).getColumnName().toUpperCase(), updateExps.toString());
                    }
                    columnSetIndexMap.put(index++, updateSet.getColumns().get(i).getColumnName().toUpperCase());
                }
            }
        } else if (statement instanceof Insert) {
            Insert insert = (Insert) statement;
            selectItemsFromUpdateSql.addAll(insert.getColumns());
            columnNameValMap.putAll(detectInsertColumnValuesNonJdbcParameters(insert));
        }
        Map<String, String> relatedColumnsUpperCaseWithoutUnderline = new HashMap<>(selectItemsFromUpdateSql.size(), 1);
        for (Column item : selectItemsFromUpdateSql) {
            //FIRSTNAME: FIRST_NAME/FIRST-NAME/FIRST$NAME/FIRST.NAME
            relatedColumnsUpperCaseWithoutUnderline.put(item.getColumnName().replaceAll("[._\-$]", "").toUpperCase(), item.getColumnName().toUpperCase());
        }
        MetaObject metaObject = SystemMetaObject.forObject(updateSql.getParameterObject());
        int index = 0;
        for (ParameterMapping parameterMapping : updateSql.getParameterMappings()) {
            String propertyName = parameterMapping.getProperty();
            if (propertyName.startsWith("ew.paramNameValuePairs")) {
                ++index;
                continue;
            }
            String[] arr = propertyName.split("\.");
            String propertyNameTrim = arr[arr.length - 1].replace("_", "").toUpperCase();
            try {
                final String columnName = columnSetIndexMap.getOrDefault(index++, getColumnNameByProperty(propertyNameTrim, tableName));
                if (relatedColumnsUpperCaseWithoutUnderline.containsKey(propertyNameTrim)) {
                    final String colkey = relatedColumnsUpperCaseWithoutUnderline.get(propertyNameTrim);
                    Object valObj = metaObject.getValue(propertyName);
                    if (valObj instanceof IEnum) {
                        valObj = ((IEnum<?>) valObj).getValue();
                    } else if (valObj instanceof Enum) {
                        valObj = getEnumValue((Enum) valObj);
                    }
                    if (columnNameValMap.containsKey(colkey)) {
                        columnNameValMap.put(relatedColumnsUpperCaseWithoutUnderline.get(propertyNameTrim), String.valueOf(columnNameValMap.get(colkey)).replace("?", valObj == null ? "" : valObj.toString()));
                    }
                    if (columnName != null && !columnNameValMap.containsKey(columnName)) {
                        columnNameValMap.put(columnName, valObj);
                    }
                } else {
                    if (columnName != null) {
                        columnNameValMap.put(columnName, String.valueOf(metaObject.getValue(propertyName)));
                    }
                }
            } catch (Exception e) {
                logger.warn("get value error,propertyName:{},parameterMapping:{}", propertyName, parameterMapping);
            }
        }
        dealWithUpdateWrapper(columnSetIndexMap, columnNameValMap, updateSql);
        return columnNameValMap;
    }

    /**
     * @param originalDataObj
     * @return
     */
    private List<DataChangedRecord> compareAndGetUpdatedColumnDatas(String tableName, OriginalDataObj originalDataObj, Map<String, Object> columnNameValMap) {
        final Set<String> ignoredColumns = ignoredTableColumns.get(tableName.toUpperCase());
        if (originalDataObj == null || originalDataObj.isEmpty()) {
            DataChangedRecord oneRecord = new DataChangedRecord();
            List<DataColumnChangeResult> updateColumns = new ArrayList<>(columnNameValMap.size());
            for (Map.Entry<String, Object> ety : columnNameValMap.entrySet()) {
                String columnName = ety.getKey();
                if ((ignoredColumns == null || !ignoredColumns.contains(columnName)) && !ignoreAllColumns.contains(columnName)) {
                    updateColumns.add(DataColumnChangeResult.constrcutByUpdateVal(columnName, ety.getValue()));
                }
            }
            oneRecord.setUpdatedColumns(updateColumns);
//            oneRecord.setUpdatedColumns(Collections.EMPTY_LIST);
            return Collections.singletonList(oneRecord);
        }
        List<DataChangedRecord> originalDataList = originalDataObj.getOriginalDataObj();
        List<DataChangedRecord> updateDataList = new ArrayList<>(originalDataList.size());
        for (DataChangedRecord originalData : originalDataList) {
            if (originalData.hasUpdate(columnNameValMap, ignoredColumns, ignoreAllColumns)) {
                updateDataList.add(originalData);
            }
        }
        return updateDataList;
    }

    private Object getEnumValue(Enum enumVal) {
        Optional<String> enumValueFieldName = MybatisEnumTypeHandler.findEnumValueFieldName(enumVal.getClass());
        if (enumValueFieldName.isPresent()) {
            return SystemMetaObject.forObject(enumVal).getValue(enumValueFieldName.get());
        }
        return enumVal;

    }

    @SuppressWarnings("rawtypes")
    private void dealWithUpdateWrapper(Map<Integer, String> columnSetIndexMap, Map<String, Object> columnNameValMap, BoundSql updateSql) {
        if (columnSetIndexMap.size() <= columnNameValMap.size()) {
            return;
        }
        MetaObject mpgenVal = SystemMetaObject.forObject(updateSql.getParameterObject());
        if(!mpgenVal.hasGetter(Constants.WRAPPER)){
            return;
        }
        Object ew = mpgenVal.getValue(Constants.WRAPPER);
        if (ew instanceof UpdateWrapper || ew instanceof LambdaUpdateWrapper) {
            final String sqlSet = ew instanceof UpdateWrapper ? ((UpdateWrapper) ew).getSqlSet() : ((LambdaUpdateWrapper) ew).getSqlSet();// columnName=#{val}
            if (sqlSet == null) {
                return;
            }
            MetaObject ewMeta = SystemMetaObject.forObject(ew);
            final Map paramNameValuePairs = (Map) ewMeta.getValue("paramNameValuePairs");
            String[] setItems = sqlSet.split(",");
            for (String setItem : setItems) {
                //age=#{ew.paramNameValuePairs.MPGENVAL1}
                String[] nameAndValuePair = setItem.split("=", 2);
                if (nameAndValuePair.length == 2) {
                    String setColName = nameAndValuePair[0].trim().toUpperCase();
                    String setColVal = nameAndValuePair[1].trim();//#{.mp}
                    if (columnSetIndexMap.containsValue(setColName)) {
                        String[] mpGenKeyArray = setColVal.split("\.");
                        String mpGenKey = mpGenKeyArray[mpGenKeyArray.length - 1].replace("}", "");
                        final Object setVal = paramNameValuePairs.get(mpGenKey);
                        if (setVal instanceof IEnum) {
                            columnNameValMap.put(setColName, String.valueOf(((IEnum<?>) setVal).getValue()));
                        } else {
                            columnNameValMap.put(setColName, String.valueOf(setVal));
                        }
                    }
                }
            }
        }
    }

    private Map<String, String> detectInsertColumnValuesNonJdbcParameters(Insert insert) {
        Map<String, String> columnNameValMap = new HashMap<>(4);
        final Select select = insert.getSelect();
        List<Column> columns = insert.getColumns();
        if (select instanceof SetOperationList) {
            SetOperationList setOperationList = (SetOperationList) select;
            final List<Select> selects = setOperationList.getSelects();
            if (CollectionUtils.isEmpty(selects)) {
                return columnNameValMap;
            }
            final Select selectBody = selects.get(0);
            if (!(selectBody instanceof Values)) {
                return columnNameValMap;
            }
            Values valuesStatement = (Values) selectBody;
            if (valuesStatement.getExpressions() instanceof ExpressionList) {
                ExpressionList expressionList = valuesStatement.getExpressions();
                List<Expression> expressions = expressionList;
                for (Expression expression : expressions) {
                    if (expression instanceof RowConstructor) {
                        final ExpressionList exprList = ((RowConstructor) expression);
                        final List<Expression> insertExpList = exprList;
                        for (int i = 0; i < insertExpList.size(); ++i) {
                            Expression e = insertExpList.get(i);
                            if (!(e instanceof JdbcParameter)) {
                                final String columnName = columns.get(i).getColumnName();
                                final String val = e.toString();
                                columnNameValMap.put(columnName, val);
                            }
                        }
                    }
                }
            }
        }
        return columnNameValMap;
    }

    private String getColumnNameByProperty(String propertyName, String tableName) {
        for (TableInfo tableInfo : TableInfoHelper.getTableInfos()) {
            if (tableName.equalsIgnoreCase(tableInfo.getTableName())) {
                final List<TableFieldInfo> fieldList = tableInfo.getFieldList();
                if (CollectionUtils.isEmpty(fieldList)) {
                    return propertyName;
                }
                for (TableFieldInfo tableFieldInfo : fieldList) {
                    if (propertyName.equalsIgnoreCase(tableFieldInfo.getProperty())) {
                        return tableFieldInfo.getColumn().toUpperCase();
                    }
                }
                return propertyName;
            }
        }
        return propertyName;
    }


    private Map<String, Object> buildParameterObjectMap(BoundSql boundSql) {
        MetaObject metaObject = PluginUtils.getMetaObject(boundSql.getParameterObject());
        Map<String, Object> propertyValMap = new HashMap<>(boundSql.getParameterMappings().size());
        for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
            String propertyName = parameterMapping.getProperty();
            if (propertyName.startsWith("ew.paramNameValuePairs")) {
                continue;
            }
            Object propertyValue = metaObject.getValue(propertyName);
            propertyValMap.put(propertyName, propertyValue);
        }
        return propertyValMap;

    }


    private List<DataChangeResult> buildOriginalData(Select selectStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        List<DataChangeResult> dataChangeResults = new ArrayList<>();
        try (PreparedStatement statement = connection.prepareStatement(selectStmt.toString())) {
            DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
            parameterHandler.setParameters(statement);
            ResultSet resultSet = statement.executeQuery();
            final ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            int count = 0;
            while (resultSet.next()) {
                ++count;
                if (checkTableBatchLimitExceeded(selectStmt, count)) {
                    logger.error("batch delete limit exceed: count={}, BATCH_UPDATE_LIMIT={}", count, BATCH_UPDATE_LIMIT);
                    throw DataUpdateLimitationException.DEFAULT;
                }
                DataChangeResult dataChangeResult = new DataChangeResult();
                List<DataChangeColumnResult> columnChangeResults = new ArrayList<>();
                dataChangeResult.setChangeColumnResults(columnChangeResults);
                for (int i = 1; i <= columnCount; ++i) {
                    DataChangeColumnResult changeColumnResult = new DataChangeColumnResult();
                    changeColumnResult.setColumnName(metaData.getColumnName(i));
                    Object res = resultSet.getObject(i);
                    if (res instanceof Clob) {
                        changeColumnResult.setOriginalValue(DataColumnChangeResult.convertClob((Clob) res));
                    } else {
                        changeColumnResult.setOriginalValue(null != res ? res.toString() : null);
                    }
                    if("id".equalsIgnoreCase(changeColumnResult.getColumnName())){
                        dataChangeResult.setId(changeColumnResult.getOriginalValue());
                    }
                    columnChangeResults.add(changeColumnResult);
                }
                dataChangeResults.add(dataChangeResult);
            }
            resultSet.close();
            return dataChangeResults;
        } catch (Exception e) {
            if (e instanceof DataUpdateLimitationException) {
                throw (DataUpdateLimitationException) e;
            }
            logger.error("try to get record tobe deleted for selectStmt={}", selectStmt, e);
            return null;
        }
    }

    private OriginalDataObj buildOriginalObjectData(Map<String, Object> updatedColumnDatas, Select selectStmt, Column pk, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        try (PreparedStatement statement = connection.prepareStatement(selectStmt.toString())) {
            DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
            parameterHandler.setParameters(statement);
            ResultSet resultSet = statement.executeQuery();
            List<DataChangedRecord> originalObjectDatas = new LinkedList<>();
            int count = 0;

            while (resultSet.next()) {
                ++count;
                if (checkTableBatchLimitExceeded(selectStmt, count)) {
                    logger.error("batch update limit exceed: count={}, BATCH_UPDATE_LIMIT={}", count, BATCH_UPDATE_LIMIT);
                    throw DataUpdateLimitationException.DEFAULT;
                }
                originalObjectDatas.add(prepareOriginalDataObj(updatedColumnDatas, resultSet, pk));
            }
            OriginalDataObj result = new OriginalDataObj();
            result.setOriginalDataObj(originalObjectDatas);
            resultSet.close();
            return result;
        } catch (Exception e) {
            if (e instanceof DataUpdateLimitationException) {
                throw (DataUpdateLimitationException) e;
            }
            logger.error("try to get record tobe updated for selectStmt={}", selectStmt, e);
            return new OriginalDataObj();
        }
    }

    /**
     * 防止出现全表批量更新
     * 默认一次更新不超过1000条
     *
     * @param selectStmt
     * @param count
     * @return
     */
    private boolean checkTableBatchLimitExceeded(Select selectStmt, int count) {
        if (!batchUpdateLimitationOpened) {
            return false;
        }
        final PlainSelect selectBody = (PlainSelect) selectStmt;
        final FromItem fromItem = selectBody.getFromItem();
        if (fromItem instanceof Table) {
            Table fromTable = (Table) fromItem;
            final String tableName = fromTable.getName().toUpperCase();
            if (!BATCH_UPDATE_LIMIT_MAP.containsKey(tableName)) {
                if (count > BATCH_UPDATE_LIMIT) {
                    logger.error("batch update limit exceed for tableName={}, BATCH_UPDATE_LIMIT={}, count={}",
                            tableName, BATCH_UPDATE_LIMIT, count);
                    return true;
                }
                return false;
            }
            final Integer limit = BATCH_UPDATE_LIMIT_MAP.get(tableName);
            if (count > limit) {
                logger.error("batch update limit exceed for configured tableName={}, BATCH_UPDATE_LIMIT={}, count={}",
                        tableName, limit, count);
                return true;
            }
            return false;
        }
        return count > BATCH_UPDATE_LIMIT;
    }


    /**
     * get records : include related column with original data in DB
     *
     * @param resultSet
     * @param pk
     * @return
     * @throws SQLException
     */
    private DataChangedRecord prepareOriginalDataObj(Map<String, Object> updatedColumnDatas, ResultSet resultSet, Column pk) throws SQLException {
        final ResultSetMetaData metaData = resultSet.getMetaData();
        int columnCount = metaData.getColumnCount();
        List<DataColumnChangeResult> originalColumnDatas = new LinkedList<>();
        DataColumnChangeResult pkval = null;
        for (int i = 1; i <= columnCount; ++i) {
            String columnName = metaData.getColumnName(i).toUpperCase();
            DataColumnChangeResult col;
            Object updateVal = updatedColumnDatas.get(columnName);
            if (updateVal != null && updateVal.getClass().getCanonicalName().startsWith("java.")) {
                col = DataColumnChangeResult.constrcutByOriginalVal(columnName, resultSet.getObject(i, updateVal.getClass()));
            } else {
                col = DataColumnChangeResult.constrcutByOriginalVal(columnName, resultSet.getObject(i));
            }
            if (pk != null && columnName.equalsIgnoreCase(pk.getColumnName())) {
                pkval = col;
            } else {
                originalColumnDatas.add(col);
            }
        }
        DataChangedRecord changedRecord = new DataChangedRecord();
        changedRecord.setOriginalColumnDatas(originalColumnDatas);
        if (pkval != null) {
            changedRecord.setPkColumnName(pkval.getColumnName());
            changedRecord.setPkColumnVal(pkval.getOriginalValue());
        }
        return changedRecord;
    }


    private Columns2SelectItemsResult buildColumns2SelectItems(String tableName, List<Column> columns) {
        if (columns == null || columns.isEmpty()) {
            return Columns2SelectItemsResult.build(Collections.singletonList(new SelectItem<>(new AllColumns())), 0);
        }
        List<SelectItem<?>> selectItems = new ArrayList<>(columns.size());
        for (Column column : columns) {
            selectItems.add(new SelectItem<>(column));
        }
        TableInfo tableInfo = getTableInfoByTableName(tableName);
        if (tableInfo == null) {
            return Columns2SelectItemsResult.build(selectItems, 0);
        }
        Column pk = new Column(tableInfo.getKeyColumn());
        selectItems.add(new SelectItem<>(pk));
        Columns2SelectItemsResult result = Columns2SelectItemsResult.build(selectItems, 1);
        result.setPk(pk);
        return result;
    }

    private String buildParameterObject(BoundSql boundSql) {
        Object paramObj = boundSql.getParameterObject();
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        if (paramObj instanceof Map) {
            Map<String, Object> paramMap = (Map<String, Object>) paramObj;
            int index = 1;
            boolean hasParamIndex = false;
            String key;
            while (paramMap.containsKey((key = "param" + index))) {
                Object paramIndex = paramMap.get(key);
                sb.append(""").append(key).append(""").append(":").append(""").append(paramIndex).append(""").append(",");
                hasParamIndex = true;
                ++index;
            }
            if (hasParamIndex) {
                sb.delete(sb.length() - 1, sb.length());
                sb.append("}");
                return sb.toString();
            }
            for (Map.Entry<String, Object> ety : paramMap.entrySet()) {
                sb.append(""").append(ety.getKey()).append(""").append(":").append(""").append(ety.getValue()).append(""").append(",");
            }
            sb.delete(sb.length() - 1, sb.length());
            sb.append("}");
            return sb.toString();
        }
        sb.append("param:").append(paramObj);
        sb.append("}");
        return sb.toString();
    }

    public OperateLogResult processDelete(Delete deleteStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        Table table = deleteStmt.getTable();
        if (checkOperateLogIgnore(table.getName())) {
            return null;
        }
        Expression where = deleteStmt.getWhere();
        PlainSelect selectBody = new PlainSelect();
        selectBody.setFromItem(table);
        selectBody.setSelectItems(Collections.singletonList(new SelectItem<>((new AllColumns()))));
        selectBody.setWhere(where);
        List<DataChangeResult> dataChangeResults = buildOriginalData(selectBody, mappedStatement, boundSql, connection);
        OperateLogResult result = new OperateLogResult();
        result.setOperation("delete");
        result.setTableName(table.getName());
        result.setRecordStatus(null != dataChangeResults);
        result.setDataChangeResults(null != dataChangeResults ? dataChangeResults : new ArrayList<>());
        return result;
    }

    /**
     * 设置批量更新记录条数上限
     *
     * @param limit
     * @return
     */
    public OperateLogMybatisInnerInterceptor setBatchUpdateLimit(int limit) {
        this.BATCH_UPDATE_LIMIT = limit;
        return this;
    }

    public OperateLogMybatisInnerInterceptor openBatchUpdateLimitation() {
        this.batchUpdateLimitationOpened = true;
        return this;
    }

    public OperateLogMybatisInnerInterceptor configTableLimitation(String tableName, int limit) {
        this.BATCH_UPDATE_LIMIT_MAP.put(tableName.toUpperCase(), limit);
        return this;
    }

    /**
     * ignoredColumns = TABLE_NAME1.COLUMN1,COLUMN2; TABLE2.COLUMN1,COLUMN2; TABLE3.*; *.COLUMN1,COLUMN2
     * 多个表用分号分隔
     * TABLE_NAME1.COLUMN1,COLUMN2 : 表示忽略这个表的这2个字段
     * TABLE3.*: 表示忽略这张表的INSERT/UPDATE,delete暂时还保留
     * *.COLUMN1,COLUMN2:表示所有表的这个2个字段名都忽略
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

        String ignoredTableColumns = properties.getProperty("ignoredTableColumns");
        if (ignoredTableColumns == null || ignoredTableColumns.trim().isEmpty()) {
            return;
        }
        String[] array = ignoredTableColumns.split(";");
        for (String table : array) {
            int index = table.indexOf(".");
            if (index == -1) {
                logger.warn("invalid data={} for ignoredColumns, format should be TABLE_NAME1.COLUMN1,COLUMN2; TABLE2.COLUMN1,COLUMN2;", table);
                continue;
            }
            String tableName = table.substring(0, index).trim().toUpperCase();
            String[] columnArray = table.substring(index + 1).split(",");
            Set<String> columnSet = new HashSet<>(columnArray.length);
            for (String column : columnArray) {
                column = column.trim().toUpperCase();
                if (column.isEmpty()) {
                    continue;
                }
                columnSet.add(column);
            }
            if ("*".equals(tableName)) {
                ignoreAllColumns.addAll(columnSet);
            } else {
                this.ignoredTableColumns.put(tableName, columnSet);
            }
        }
    }

    public List<DataChangeResult> buildDataStr(List<DataChangedRecord> records) {
        List<DataChangeResult> dataChangeResults = new ArrayList<>();
        for (DataChangedRecord r : records) {
            DataChangeResult dataChangeResult = new DataChangeResult();
            dataChangeResult.setId(null != r.getPkColumnVal() ? r.getPkColumnVal().toString() : null);
            List<DataChangeColumnResult> changeColumnResults = new ArrayList<>();
            dataChangeResult.setChangeColumnResults(changeColumnResults);
            for (DataColumnChangeResult updatedColumn : r.getUpdatedColumns()) {
                DataChangeColumnResult changeColumnResult = new DataChangeColumnResult();
                changeColumnResult.setColumnName(updatedColumn.getColumnName());
                changeColumnResult.setOriginalValue(updatedColumn.convertDoubleQuotes(updatedColumn.getOriginalValue()));
                changeColumnResult.setUpdateValue(updatedColumn.convertDoubleQuotes(updatedColumn.getUpdateValue()));
                changeColumnResults.add(changeColumnResult);
            }
            dataChangeResults.add(dataChangeResult);
        }
        return dataChangeResults;
    }



    @Data
    public static class Columns2SelectItemsResult {

        private Column pk;
        /**
         * all column with additional columns: ID, etc.
         */
        private List<SelectItem<?>> selectItems;
        /**
         * newly added column count from meta data.
         */
        private int additionalItemCount;

        public static Columns2SelectItemsResult build(List<SelectItem<?>> selectItems, int additionalItemCount) {
            Columns2SelectItemsResult result = new Columns2SelectItemsResult();
            result.setSelectItems(selectItems);
            result.setAdditionalItemCount(additionalItemCount);
            return result;
        }
    }

    @Data
    public static class OriginalDataObj {

        private List<DataChangedRecord> originalDataObj;

        public boolean isEmpty() {
            return originalDataObj == null || originalDataObj.isEmpty();
        }

    }

    @Data
    public static class DataColumnChangeResult {

        private String columnName;
        private Object originalValue;
        private Object updateValue;

        @SuppressWarnings("rawtypes")
        public boolean isDataChanged(Object updateValue) {
            if (!Objects.equals(originalValue, updateValue)) {
                if (originalValue instanceof Clob) {
                    String originalStr = convertClob((Clob) originalValue);
                    setOriginalValue(originalStr);
                    return !originalStr.equals(updateValue);
                }
                if (originalValue instanceof Comparable) {
                    Comparable original = (Comparable) originalValue;
                    Comparable update = (Comparable) updateValue;
                    try {
                        return update == null || original.compareTo(update) != 0;
                    } catch (Exception e) {
                        return true;
                    }
                }
                return true;
            }
            return false;
        }

        public static String convertClob(Clob clobObj) {
            try {
                return clobObj.getSubString(0, (int) clobObj.length());
            } catch (Exception e) {
                try (Reader is = clobObj.getCharacterStream()) {
                    char[] chars = new char[64];
                    int readChars;
                    StringBuilder sb = new StringBuilder();
                    while ((readChars = is.read(chars)) != -1) {
                        sb.append(chars, 0, readChars);
                    }
                    return sb.toString();
                } catch (Exception e2) {
                    //ignored
                    return "unknown clobObj";
                }
            }
        }

        public static DataColumnChangeResult constrcutByUpdateVal(String columnName, Object updateValue) {
            DataColumnChangeResult res = new DataColumnChangeResult();
            res.setColumnName(columnName);
            res.setUpdateValue(updateValue);
            return res;
        }

        public static DataColumnChangeResult constrcutByOriginalVal(String columnName, Object originalValue) {
            DataColumnChangeResult res = new DataColumnChangeResult();
            res.setColumnName(columnName);
            res.setOriginalValue(originalValue);
            return res;
        }

        public String generateDataStr() {
            StringBuilder sb = new StringBuilder();
            sb.append(""").append(columnName).append(""").append(":").append(""").append(convertDoubleQuotes(originalValue)).append("->").append(convertDoubleQuotes(updateValue)).append(""").append(",");
            return sb.toString();
        }

        public String convertDoubleQuotes(Object obj) {
            if (obj == null) {
                return null;
            }
            return obj.toString().replace(""", "\"");
        }
    }

    @Data
    public static class DataChangedRecord {

        private String pkColumnName;
        private Object pkColumnVal;
        private List<DataColumnChangeResult> originalColumnDatas;
        private List<DataColumnChangeResult> updatedColumns;

        public boolean hasUpdate(Map<String, Object> columnNameValMap, Set<String> ignoredColumns, Set<String> ignoreAllColumns) {
            if (originalColumnDatas == null) {
                return true;
            }
            boolean hasUpdate = false;
            updatedColumns = new ArrayList<>(originalColumnDatas.size());
            for (DataColumnChangeResult originalColumn : originalColumnDatas) {
                final String columnName = originalColumn.getColumnName().toUpperCase();
                if (ignoredColumns != null && ignoredColumns.contains(columnName) || ignoreAllColumns.contains(columnName)) {
                    continue;
                }
                Object updatedValue = columnNameValMap.get(columnName);
                if (originalColumn.isDataChanged(updatedValue)) {
                    hasUpdate = true;
                    originalColumn.setUpdateValue(updatedValue);
                    updatedColumns.add(originalColumn);
                }
            }
            return hasUpdate;
        }

        public String generateUpdatedDataStr() {
            StringBuilder sb = new StringBuilder();
            sb.append("{");
            if (pkColumnName != null) {
                sb.append(""").append(pkColumnName).append(""").append(":").append(""").append(convertDoubleQuotes(pkColumnVal)).append(""").append(",");
            }
            for (DataColumnChangeResult update : updatedColumns) {
                sb.append(update.generateDataStr());
            }
            sb.replace(sb.length() - 1, sb.length(), "}");
            return sb.toString();
        }

        public String convertDoubleQuotes(Object obj) {
            if (obj == null) {
                return null;
            }
            return obj.toString().replace(""", "\"");
        }
    }

    public static class DataUpdateLimitationException extends MybatisPlusException {

        public DataUpdateLimitationException(String message) {
            super(message);
        }

        public static DataUpdateLimitationException DEFAULT = new DataUpdateLimitationException("本次操作 因超过系统安全阈值 被拦截,如需继续,请联系管理员!");
    }

    public void setPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
}

MybatisPlusConfig

java 复制代码
@Configuration
public class MybatisPlusConfig {

    @Autowired
    private ApplicationEventPublisher publisher;
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        OperateLogMybatisInnerInterceptor operateLogMybatisInnerInterceptor = new OperateLogMybatisInnerInterceptor();
        // 配置安全阈值,例如限制批量更新或插入的记录数不超过 1000 条
        operateLogMybatisInnerInterceptor.setBatchUpdateLimit(1000);
        operateLogMybatisInnerInterceptor.setPublisher(publisher);
        interceptor.addInnerInterceptor(operateLogMybatisInnerInterceptor);
        return interceptor;
    }
}

Swagger配置

less 复制代码
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket petApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .groupName("日志审计")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.huzhihui.data.change.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo(){
        Contact contact = new Contact("huzhihui", "https://xxx.com/", "xxx@qq.com");
        return new ApiInfo(
                "日志审计",
                "日志审计DEMO",
                "v1.0",
                "https://xxx.com/",
                contact,
                "Apache 2.0",
                "https://www.apache.org/licenses/LICENSE-2.0",
                new ArrayList());
    }
}

测试方法

less 复制代码
@Api(tags = {"用户信息"})
@RestController
@RequestMapping("userinfo")
public class UserinfoController {

    @Autowired
    private UserinfoService userinfoService;
    @Autowired
    private UserinfoMapper userinfoMapper;
    @Autowired
    private SubMapper subMapper;

    @OperateLog("新增")
    @ApiOperation(value = "新增", extensions = {@Extension(name = "", properties = {@ExtensionProperty(name = "", value = "")})})
    @Transactional(rollbackFor = Exception.class)
    @PostMapping(value = "add")
    public Object add(@RequestBody UserinfoEntity entity){
        userinfoService.save(entity);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "batchAdd")
    public Object batchAdd(int count, String preName){
        List<UserinfoEntity> entities = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            UserinfoEntity entity = new UserinfoEntity();
            entity.setName(preName + i);
            entities.add(entity);
        }
        userinfoService.saveBatch(entities, count);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @PostMapping(value = "update")
    public Object update(@RequestBody UserinfoEntity entity){
        userinfoService.updateById(entity);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "delete")
    public Object delete(Long id){
        userinfoService.removeById(id);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "otherUpdate1")
    public Object otherUpdate1(String name){
        LambdaUpdateWrapper<UserinfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(UserinfoEntity::getName, name + new Date())
                .set(UserinfoEntity::getUpdateTime, new Date())
                .eq(UserinfoEntity::getName, name);
        userinfoService.update(updateWrapper);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "otherUpdate2")
    public Object otherUpdate2(String name){
        UserinfoEntity userinfoEntity = new UserinfoEntity();
        userinfoEntity.setMoney(new BigDecimal(1));
        userinfoEntity.setCode("T1");
        LambdaUpdateWrapper<UserinfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(UserinfoEntity::getName, name + new Date())
                .set(UserinfoEntity::getCode, "C1")
                .set(UserinfoEntity::getUpdateTime, new Date())
                .eq(UserinfoEntity::getName, name);
        userinfoService.update(userinfoEntity, updateWrapper);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "complexUpdate1")
    public Object complexUpdate1(Long id, String code){
        userinfoMapper.complexUpdate(code,id);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "complexUpdate2")
    public Object complexUpdate2(Long id){
        userinfoMapper.complexUpdate2(id);
        return "SUCCESS";
    }

    @GetMapping(value = "tableInfo")
    public Object tableInfo(String tableName){
        TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
        return tableInfo;
    }

    @OperateLog("复合新增")
    @GetMapping(value = "complexAdd")
    public Object complexAdd(){
        UserinfoEntity entity = new UserinfoEntity();
        entity.setId(IdWorker.getId());
        entity.setName("XXX");
        userinfoMapper.insert(entity);

        SubEntity subEntity = new SubEntity();
        subEntity.setId(IdWorker.getId());
        subEntity.setParentId(entity.getId());
        subMapper.insert(subEntity);
        return "SUCCESS";
    }

}

测试结果

相关推荐
旋风菠萝9 小时前
JVM易混淆名称
java·jvm·数据库·spring boot·redis·面试
77qqqiqi10 小时前
解决Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required报错问题
java·数据库·微服务·mybatis·mybatisplus
weisian15110 小时前
Java WEB技术-序列化和反序列化认识(SpringBoot的Jackson序列化行为?如何打破序列化过程的驼峰规则?如何解决学序列化循环引用问题?)
java·spring boot
橘子编程10 小时前
SpringMVC核心原理与实战指南
java·spring boot·spring·tomcat·mybatis
Savvy..11 小时前
Day07 JDBC+MyBatis
mybatis·jdbc·数据库连接池·sql注入·yml
找不到、了11 小时前
关于MyBatis 的懒加载(Lazy Loading)机制
java·mybatis
慌糖12 小时前
Spring Boot音乐服务器项目-查询喜欢的音乐模块
服务器·spring boot·mybatis
love静思冥想14 小时前
MyBatis XML 配置方式是 返回 Boolean 类型
xml·mybatis
尚学教辅学习资料15 小时前
SpringBoot3.x入门到精通系列:1.2 开发环境搭建
spring boot·gradle·maven·intellij idea·jdk17·开发环境
找不到、了15 小时前
Kafka在Springboot项目中的实践
spring boot·分布式·kafka