SpringCloud或SpringBoot+Mybatis-Plus+ThreadLocal利用AOP+mybatis插件实现数据操作记录及更新对比

引文

  本文主要介绍如何使用Spring AOP + mybatis插件实现拦截数据库操作并根据不同需求进行数据对比分析,主要适用于系统中需要对数据操作进行记录、在更新数据时准确记录更新字段

核心:ThreadLocal、AOP、mybatis插件(拦截器)、mybatis-Plus实体规范、数据对比

实现思路

  1. 使用注解DataLog标记需要记录操作日志的接口或方法
  2. 进入Aop进行数据初始化,告知mybatis拦截器该线程操作需要记录操作记录,并使用ThreadLocal进行线程隔离,防止多线程操作时引发记录错乱问题
  3. mybatis插件进行更新sql拦截并记录更新前数据
  4. 数据更新完成并在接口或方法正确返回后再次查询更新后数据
  5. 进行更新前后数据对比并调用上层处理接口进行数据记录

1、相关技术简介

mybatis插件:

  mybatis插件实际上就是官方针对4层数据操作处理预留的拦截器,使用者可以根据不同的需求进行操作拦截并处理。这边笔者不做详细描述,详细介绍请到官网了解,这里笔者就复用官网介绍。

插件(plugins)
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为如果在试图修改或重写已有方法的行为的时候,你很可能在破坏 MyBatis 的核心模块。 这些都是更低层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行低层映射语句的内部对象。

提示 覆盖配置类

除了用插件来修改 MyBatis 核心行为之外,还可以通过完全覆盖配置类来达到目的。只需继承后覆盖其中的每个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会严重影响 MyBatis 的行为,务请慎之又慎。

重点讲下4层处理,MyBatis两级缓存就是在其中两层中实现

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    所有数据库操作到达底层后都由该执行器进行任务分发,主要有update(插入、更新、删除),query(查询),提交,回滚,关闭链接等
  • ParameterHandler (getParameterObject, setParameters)
    参数处理器(获取参数,设置参数)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
    结果集处理器(结果集,输出参数)
  • StatementHandler (prepare, parameterize, batch, update, query)
    声明处理器、准备链接jdbc前处理,prepare(预处理):生成sql语句,准备链接数据库进行操作

以上4层执行顺序为顺序执行

  • Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。
  • ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置。
  • ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口对象。我们可以定义插件对Mybatis的结果集自动映射进行修改。
  • StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。

MyBatis-Plus:
  MyBatis增强器,主要规范了数据实体,在底层实现了简单的增删查改,使用者不再需要开发基础操作接口,小编认为是最强大、最方便易用的,没有之一,不接受任何反驳。详细介绍请看官网

数据实体的规范让底层操作更加便捷,本例主要实体规范中的表名以及主键获取,下面上实体规范demo

package com.lith.datalog.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 * 用户表
 * </p>
 *
 * @author Tophua
 * @since 2020/5/7
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends Model<User> {
    /**
     * 主键id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
    private String email;
}

2、实现

本文所要讲述的就是在第四级(StatementHandler)进行拦截并实现数据对比记录。
本例为公共模块实现,然后在其它模块中依赖此公共模块,根据每个模块不同的需求自定义实现不同的处理。

结构目录
在这里插入图片描述

一、配置

package com.lith.datalog.config;

import com.lith.datalog.handle.BaseDataLog;
import com.lith.datalog.handle.DataUpdateInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * <p>
 * Mybatis-Plus配置
 * </p>
 *
 * @author Tophua
 * @since 2020/5/7
 */
@Configuration
@EnableTransactionManagement
@MapperScan("com.lith.**.mapper")
public class MybatisPlusConfig {

    /**
     * <p>
     * SQL执行效率插件  设置 dev test 环境开启
     * </p>
     *
     * @return cn.rc100.common.data.mybatis.EplusPerformanceInterceptor
     * @author Tophua
     * @since 2020/3/11
     */
    @Bean
    @Profile({"dev","test"})
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }

    /**
     * <p>
     * 数据更新操作处理
     * </p>
     *
     * @return com.lith.datalog.handle.DataUpdateInterceptor
     * @author Tophua
     * @since 2020/5/11
     */
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean(BaseDataLog.class)
    public DataUpdateInterceptor dataUpdateInterceptor() {
        return new DataUpdateInterceptor();
    }
}

二、实现拦截器

DataUpdateInterceptor,根据官网demo实现拦截器,在拦截器中根据增、删、改操作去调用各个模块中自定义实现的处理方法来达到不同的操作处理。

package com.lith.datalog.handle;

import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.lith.datalog.annotation.IgnoreDataLog;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils;

import java.lang.reflect.Proxy;
import java.sql.Statement;
import java.util.*;

/**
 * <p>
 * 数据更新拦截器
 * </p>
 *
 * @author Tophua
 * @since 2020/5/11
 */
@Slf4j
@AllArgsConstructor
@Intercepts({@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})
public class DataUpdateInterceptor extends AbstractSqlParserHandler implements Interceptor {

    @Override
    @SneakyThrows
    public Object intercept(Invocation invocation) {
        // 判断是否需要记录日志
        if (BaseDataLog.DATA_CHANGES.get() == null) {
            return invocation.proceed();
        }
        Statement statement;
        Object firstArg = invocation.getArgs()[0];
        if (Proxy.isProxyClass(firstArg.getClass())) {
            statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
        } else {
            statement = (Statement) firstArg;
        }
        MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
        try {
            statement = (Statement) stmtMetaObj.getValue("stmt.statement");
        } catch (Exception e) {
            // do nothing
        }
        if (stmtMetaObj.hasGetter("delegate")) {
            //Hikari
            try {
                statement = (Statement) stmtMetaObj.getValue("delegate");
            } catch (Exception ignored) {

            }
        }

        String originalSql = statement.toString();
        originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE);
        int index = indexOfSqlStart(originalSql);
        if (index > 0) {
            originalSql = originalSql.substring(index);
        }
        System.err.println("执行SQL:" + originalSql);

        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        this.sqlParser(metaObject);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        // 获取执行Sql
        String sql = originalSql.replace("where", "WHERE");
        // 插入
        if (SqlCommandType.INSERT.equals(mappedStatement.getSqlCommandType())) {
        }
        // 更新
        if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType())) {
            // 使用mybatis-plus 工具解析sql获取表名
            Collection<String> tables = new TableNameParser(sql).tables();
            if (CollectionUtils.isEmpty(tables)) {
                return invocation.proceed();
            }
            String tableName = tables.iterator().next();
            // 排除表名判断
            if (BaseDataLog.excludeTableNames.contains(tableName)) {
                return invocation.proceed();
            }
            // 使用mybatis-plus 工具根据表名找出对应的实体类
            TableInfo tableInfo = Optional.ofNullable(TableInfoHelper.getTableInfo(tableName))
                    .orElse(new TableInfo(null));
            Class<?> entityType = tableInfo.getEntityType();

            if (entityType == null || entityType.isAnnotationPresent(IgnoreDataLog.class)) {
                return invocation.proceed();
            }
            DataChange change = new DataChange();
            change.setTableName(tableName);
            change.setEntityType(entityType);
            // 设置sql用于执行完后查询新数据
            String selectSql = "AND " + sql.substring(sql.lastIndexOf("WHERE") + 5);
            // 同表对同条数据操作多次只进行一次对比
            if (BaseDataLog.DATA_CHANGES.get().stream().anyMatch(c -> tableName.equals(c.getTableName())
                    && selectSql.equals(c.getWhereSql()))) {
                return invocation.proceed();
            }
            change.setWhereSql(selectSql);
            Map<String, Object> map = new HashMap<>(1);
            map.put(Constants.WRAPPER, Wrappers.query().eq("1", 1).last(selectSql));
            // 查询更新前数据
            SqlSessionFactory sqlSessionFactory = SqlHelper.sqlSessionFactory(entityType);
            change.setSqlSessionFactory(sqlSessionFactory);
            change.setSqlStatement(tableInfo.getSqlStatement(SqlMethod.SELECT_LIST.getMethod()));
            SqlSession sqlSession = sqlSessionFactory.openSession();
            try {
                List<?> oldData = sqlSession.selectList(change.getSqlStatement(), map);
                change.setOldData(Optional.ofNullable(oldData).orElse(new ArrayList<>()));
            } finally {
                SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
            }
            BaseDataLog.DATA_CHANGES.get().add(change);
        }
        // 删除
        if (SqlCommandType.DELETE.equals(mappedStatement.getSqlCommandType())) {
        }
        return invocation.proceed();
    }

    /**
     * 获取sql语句开头部分
     *
     * @param sql ignore
     * @return ignore
     */
    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        Set<Integer> set = new HashSet<>();
        set.add(upperCaseSql.indexOf("SELECT "));
        set.add(upperCaseSql.indexOf("UPDATE "));
        set.add(upperCaseSql.indexOf("INSERT "));
        set.add(upperCaseSql.indexOf("DELETE "));
        set.remove(-1);
        if (CollectionUtils.isEmpty(set)) {
            return -1;
        }
        List<Integer> list = new ArrayList<>(set);
        list.sort(Comparator.naturalOrder());
        return list.get(0);
    }
}

三、AOP切面实现

使用AOP主要是考虑到一个方法中会出现多次数据库操作,而这些操作在记录中只能算作用户的一次操作,故使用AOP进行操作隔离,将一个方法内的所有数据库操作合并为一次记录。

此外AOP还代表着是否需要记录日志,有切点才会进行记录。

AOP 切点注解

package com.lith.datalog.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * <p>
 * 数据日志注解
 * </p>
 *
 * @author Tophua
 * @since 2020/7/27
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataLog {
    /**
     * sPel表达式1
     */
    String sPel1() default "";

    /**
     * sPel表达式2
     */
    String sPel2() default "";

    /**
     * sPel表达式3
     */
    String sPel3() default "";

    /**
     * <p>
     * 类型
     * </p>
     *
     * @return int
     * @author Tophua
     * @since 2020/8/11
     */
    int type() default -1;

    /**
     * <p>
     * 标签
     * </p>
     *
     * @return java.lang.String
     * @author Tophua
     * @since 2020/8/12
     */
    String tag() default "";

    /**
     * <p>
     * 注释
     * </p>
     *
     * @return java.lang.String
     * @author Tophua
     * @since 2020/8/11
     */
    String note() default "";
}

Aop切面处理

package com.lith.datalog.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.lith.datalog.annotation.DataLog;
import com.lith.datalog.handle.BaseDataLog;
import com.lith.datalog.handle.DataChange;
import lombok.AllArgsConstructor;
import org.apache.ibatis.session.SqlSession;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * DataLog切面
 * </p>
 *
 * @author Tophua
 * @since 2020/7/15
 */
@Aspect
@Order(99)
@Component
@AllArgsConstructor
public class DataLogAspect {

    private final BaseDataLog baseDataLog;

    /**
     * <p>
     * 初始化
     * </p>
     *
     * @return void
     * @author Tophua
     * @since 2020/10/30
     */
    @PostConstruct
    public void init() {
        baseDataLog.setting();
    }

    /**
     * <p>
     * 切面前执行
     * </p>
     *
     * @param dataLog dataLog
     * @return void
     * @author Tophua
     * @since 2020/7/15
     */
    @Before("@annotation(dataLog)")
    public void before(JoinPoint joinPoint, DataLog dataLog) {
        // 使用 ThreadLocal 记录一次操作
        BaseDataLog.DATA_CHANGES.set(new LinkedList<>());
        BaseDataLog.JOIN_POINT.set(joinPoint);
        BaseDataLog.DATA_LOG.set(dataLog);
        if (baseDataLog.isIgnore(dataLog)) {
            BaseDataLog.DATA_CHANGES.set(null);
        }
    }

    /**
     * <p>
     * 切面后执行
     * </p>
     *
     * @param dataLog dataLog
     * @return void
     * @author Tophua
     * @since 2020/7/15
     */
    @AfterReturning("@annotation(dataLog)")
    public void after(DataLog dataLog) {
        List<DataChange> list = BaseDataLog.DATA_CHANGES.get();
        if (CollUtil.isEmpty(list)) {
            return;
        }
        list.forEach(change -> {
            List<?> oldData = change.getOldData();
            if (CollUtil.isEmpty(oldData)) {
                return;
            }
            List<Long> ids = oldData.stream()
                    .map(o -> ReflectUtil.invoke(o, "getId").toString())
                    .filter(ObjectUtil::isNotNull)
                    .map(Long::parseLong)
                    .collect(Collectors.toList());
            SqlSession sqlSession = change.getSqlSessionFactory().openSession();
            try {
                Map<String, Object> map = new HashMap<>(1);
                map.put(Constants.WRAPPER, Wrappers.query().in("id", ids));
                List<?> newData = sqlSession.selectList(change.getSqlStatement(), map);
                change.setNewData(Optional.ofNullable(newData).orElse(new ArrayList<>()));
            } finally {
                SqlSessionUtils.closeSqlSession(sqlSession, change.getSqlSessionFactory());
            }
            System.out.println("oldData:" + JSONUtil.toJsonStr(change.getOldData()));
            System.out.println("newData:" + JSONUtil.toJsonStr(change.getNewData()));
        });
        // 对比调模块
        this.compareAndTransfer(list);
    }

    /**
     * <p>
     * 对比保存
     * </p>
     *
     * @param list list
     * @return void
     * @author Tophua
     * @since 2020/7/15
     */
    public void compareAndTransfer(List<DataChange> list) {
        StringBuilder sb = new StringBuilder();
        StringBuilder rsb = new StringBuilder();
        list.forEach(change -> {
            List<?> oldData = change.getOldData();
            List<?> newData = change.getNewData();
            // 更新前后数据量不对必定是删除(逻辑删除)不做处理
            if (newData == null) {
                return;
            }
            if (oldData == null) {
                return;
            }
            if (oldData.size() != newData.size()) {
                return;
            }
            // 按id排序
            oldData.sort(Comparator.comparingLong(d -> Long.parseLong(ReflectUtil.invoke(d, "getId").toString())));
            newData.sort(Comparator.comparingLong(d -> Long.parseLong(ReflectUtil.invoke(d, "getId").toString())));

            for (int i = 0; i < oldData.size(); i++) {
                final int[] finalI = {0};
                baseDataLog.sameClazzDiff(oldData.get(i), newData.get(i)).forEach(r -> {
                    String oldV = r.getOldValue() == null ? "无" : r.getOldValue().toString();
                    String newV = r.getNewValue() == null ? "无" : r.getNewValue().toString();
                    if (ObjectUtil.equal(oldV.trim(), newV.trim())) {
                        return;
                    }
                    if (finalI[0] == 0) {
                        sb.append(StrUtil.LF);
                        sb.append(StrUtil.format("修改表:【{}】", change.getTableName()));
                        sb.append(StrUtil.format("id:【{}】", r.getId()));
                    }
                    sb.append(StrUtil.LF);
                    rsb.append(StrUtil.LF);
                    sb.append(StrUtil.format("把字段[{}]从[{}]改为[{}]",
                            r.getFieldName(), r.getOldValue(), r.getNewValue()));
                    rsb.append(StrUtil.indexedFormat(baseDataLog.getLogFormat(),
                            r.getId(), r.getFieldName(), r.getFieldComment(),
                            oldV, newV));
                    finalI[0]++;
                });
            }
        });
        if (sb.length() > 0) {
            sb.deleteCharAt(0);
            rsb.deleteCharAt(0);
        }
        // 存库
        System.err.println(sb.toString());

        BaseDataLog.DATA_CHANGES.set(list);
        BaseDataLog.LOG_STR.set(rsb.toString());
        baseDataLog.transfer();
    }

}

3、测试及结果

经过测试,不管怎么使用数据更新操作,结果都可以进行拦截记录,完美达到预期。

小笔这里并没有将记录保存在数据库,由大家自行保存。

测试demo

package com.lith.datalog.controller;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.lith.datalog.annotation.DataLog;
import com.lith.datalog.entity.User;
import com.lith.datalog.mapper.UserMapper;
import com.lith.datalog.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * UserController
 * </p>
 *
 * @author Tophua
 * @since 2020/5/7
 */
@RestController
@AllArgsConstructor
@RequestMapping("/user")
public class UserController {

    private final UserService userService;
    private final UserMapper userMapper;

    @GetMapping("{id}")
    public User getById(@PathVariable Integer id) {
        return userService.getById(id);
    }

    @DataLog
    @PostMapping
    public Boolean save(@RequestBody User user) {
        return userService.save(user);
    }

    @DataLog
    @PutMapping
    @Transactional(rollbackFor = Exception.class)
    public Boolean updateById(@RequestBody User user) {
        User nUser = new User();
        nUser.setId(2);
        nUser.setName("代码更新");
        nUser.updateById();
        userService.update(Wrappers.<User>lambdaUpdate()
                .set(User::getName, "批量")
                .in(User::getId, 3, 4));
        userMapper.updateTest();
        return userService.updateById(user);
    }

    @DataLog
    @DeleteMapping("{id}")
    public Boolean removeById(@PathVariable Integer id) {
        return userService.removeById(id);
    }
}

结果显示:

Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.updateById
Execute SQL:UPDATE user SET name='代码更新' WHERE id=2

 Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.update
Execute SQL:UPDATE user SET name='批量' WHERE (id IN (3,4))

 Time:2 ms - ID:com.lith.datalog.mapper.UserMapper.updateTest
Execute SQL:update user set age = 44 where id in (5,6)

 Time:0 ms - ID:com.lith.datalog.mapper.UserMapper.updateById
Execute SQL:UPDATE user SET name='4564', age=20, email='dsahkdhkashk' WHERE id=1

oldData:[{"name":"1","id":2,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"代码更新","id":2,"age":10,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":3,"age":10,"email":"dsahkdhkashk"},{"name":"1","id":4,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"批量","id":3,"age":10,"email":"dsahkdhkashk"},{"name":"批量","id":4,"age":10,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":5,"age":10,"email":"dsahkdhkashk"},{"name":"1","id":6,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"1","id":5,"age":44,"email":"dsahkdhkashk"},{"name":"1","id":6,"age":44,"email":"dsahkdhkashk"}]
oldData:[{"name":"1","id":1,"age":10,"email":"dsahkdhkashk"}]
newData:[{"name":"4564","id":1,"age":20,"email":"dsahkdhkashk"}]
修改表:【user】id:【2】
把字段[name]从[1]改为[代码更新]
修改表:【user】id:【3】
把字段[name]从[1]改为[批量]
修改表:【user】id:【4】
把字段[name]从[1]改为[批量]
修改表:【user】id:【5】
把字段[age]从[10]改为[44]
修改表:【user】id:【6】
把字段[age]从[10]改为[44]
修改表:【user】id:【1】
把字段[name]从[1]改为[4564]
把字段[age]从[10]改为[20]

4、额外功能

  1. @IgnoreDataLog 注解可用于实体类上或某字段上以实现某表某字段不进行数据更新记录
  2. 可自定义枚举数据字典翻译
  3. 可在DataLogHandle类的setting()初始化方法中调用父类方法以实现设置,其中支持:
    • setLogFormat(String logFormat) 设置已定义操作记录文字翻译模板
    • addExcludeTableName(String tableName)setExcludeTableNames(List<String> tableNames) 设置排除某些表,与@IgnoreDataLog类似
    • addExcludeFieldName(String fieldName)setExcludeFieldNames(List<String> fieldNames) 设置排除某些字段,与@IgnoreDataLog类似
  4. 重写isIgnore(DataLog dataLog) 方法以达到是否忽略某次操作
  5. 还可调用父类protected <T> T getValueBySpEl(String spEl, Class<T> clazz) 方法解析@DataLog注解中所使用的的Spel表达式获取数据

4、总结

本次综合前车经验,优化设计思想,改为从底层具体执行的 sql 语句入手,通过解析表名及更新条件来构造数据更新前后的查询sql,再使用Spring AOP对方法执行前后进行处理,记录更新前后的数据。最后再使用java反射机制将数据更新前后进行对比记录。

同时使用ThreadLocal处理多线程问题,保证一个BaseDataLog Bean下多线程操作不会发生数据错乱。

使用AOP涉及到一点,就是需要保证AOP与Spring 数据库事务之间的执行顺序,如果AOP先执行然后再提交事务,那结果则是数据无变化。

在此小笔已将AOP处理级别放到最后,保证先提交事务再去查询更新后的数据,这样才能得出正确的结果。

本例采用的数据库是Mysql、连接池采用com.alibaba.druid.pool.DruidDataSource,如使用其他数据库或连接池出现sql截取不正常或无参数需根据mybatis-plus版本重新分析底层处理、再进行sql截取部分代码的重新开发。





欢迎各路大神交流意见。。。。。。



最后附上源码地址:

https://gitee.com/TopSkyhua/datalog

posted @ 2020-07-16 11:31  TopSkyhua  阅读(5725)  评论(34编辑  收藏  举报