Spring AOP实现记录mybatis-plus操作数据变化

文章目录

  • 简介
  • 功能实现
    • 定义AOP拦截器MybatisPlusMethodInterceptor
    • 实现DataLogAspectSupport数据记录支持
    • MybatisPlusDataLogConfiguration启用配置
    • 运行效果
  • maven依赖参考
  • 总结

简介

本文记录通过spring aop实现记录mybatis-plus mapper接口,在执行删除及修改数据时操作日志,自动比对历史数据. 分析字段数据变化. 可以作为实现审计日志功能实现基础技术

功能实现

定义AOP拦截器MybatisPlusMethodInterceptor

MybatisPlusMethodInterceptor 实现MethodInterceptor接口,作为方法增强的入口.

public class MybatisPlusMethodInterceptor extends DataLogAspectSupport implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 非BaseMapper类型跳过增强
        if (!(invocation.getThis() instanceof BaseMapper)) {
            return invocation.proceed();
        }
        Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
       return invoke(invocation.getThis(), targetClass, invocation.getMethod(), invocation.getArguments(), invocation::proceed);
    }
}

DataLogAspectSupport封装实现日志记录逻辑

实现DataLogAspectSupport数据记录支持

由于mybatis-plus的mapper接口都是实现BaseMapper

public interface AccountMapper extends BaseMapper<Account> {
}

利用这个特点可以实现统一逻辑处理. update和delete操作,结合反射获取mybatis-plus注解@TableId

@TableName("account")
public class Account {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private String username;
    //setter/getter省略
}

即可获取当前记录的主键,从而自动查询操作前数据,生成比对信息

public class DataLogAspectSupport {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataLogAspectSupport.class);

    public Object invoke(Object target, Class<?> targetClass, Method method, Object[] args,
                            final InvocationCallback invocation) throws Throwable {
        LogInfo logInfo = this.recordLog(target, targetClass, method, args);
        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = invocation.proceedWithLog();
        } catch (Throwable t) {
            if (logInfo != null) {
                logInfo.setException(ExceptionUtils.getRootCause(t).getMessage());
            }
        } finally {
            long timeout = System.currentTimeMillis() - startTime;
            logInfo.setTimeout(timeout);
        }
        LOGGER.info("[easy-log]{}:\n{}", logInfo.getMethod(), JSON.toJSONString(logInfo, true));
        return result;
    }

    private LogInfo recordLog(Object target, Class<?> targetClass, Method method, Object[] args) {
        LogInfo logInfo = new LogInfo();
        BaseMapper baseMapper = (BaseMapper) target;
        String methodName = ((Class)targetClass.getGenericInterfaces()[0]).getSimpleName() + "." + method.getName();
        logInfo.setMethod(methodName);
        // TODO 后续支持更多方法
        if (methodName.contains("updateById") || methodName.contains("deleteById")) {
            LOGGER.debug("[easy-log][{}] 执行数据变化分析--开始", methodName);
            Serializable primaryKey = this.getPrimaryKey(args[0]);
            LOGGER.debug("[easy-log][{}] key:[{}] current:{}", methodName, primaryKey, JSON.toJSONString(args[0]));
            Object result = baseMapper.selectById(primaryKey);
            LOGGER.debug("[easy-log][{}] key:[{}] history:{}", methodName, primaryKey, JSON.toJSONString(result));
            if (methodName.contains("updateById")){
                try {
                    List<CompareResult> compareResultList = this.compareTowObject(result, args[0]);
                    logInfo.setDataSnapshot(JSON.toJSONString(compareResultList));
                    LOGGER.debug("[easy-log][{}] key:[{}] compareResult:" + JSON.toJSONString(compareResultList), methodName, primaryKey);
                    for (CompareResult compareResult : compareResultList) {
                        String report = compareResult.getFieldName() + "【" + compareResult.getFieldComment() + "】值:" + compareResult.getOldValue() + " => " + compareResult.getNewValue();
                        LOGGER.debug(report);
                    }
                    LOGGER.debug("[easy-log][{}] 执行数据变化分析--结束", methodName);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            } else {
                logInfo.setDataSnapshot(JSON.toJSONString(result));
            }

        }
        return logInfo;
    }


    /**
     * 对比两个对象
     *
     * @param oldObj 旧对象
     * @param newObj 新对象
     */
    protected List<CompareResult> compareTowObject(Object oldObj, Object newObj) throws IllegalAccessException {
        List<CompareResult> list = new ArrayList<>();
        //获取对象的class
        Class<?> clazz1 = oldObj.getClass();
        Class<?> clazz2 = newObj.getClass();
        //获取对象的属性列表
        Field[] field1 = clazz1.getDeclaredFields();
        Field[] field2 = clazz2.getDeclaredFields();
        //遍历属性列表field1
        for (int i = 0; i < field1.length; i++) {
            //遍历属性列表field2
            for (int j = 0; j < field2.length; j++) {
                //如果field1[i]属性名与field2[j]属性名内容相同
                if (field1[i].getName().equals(field2[j].getName())) {
                    field1[i].setAccessible(true);
                    field2[j].setAccessible(true);
                    if (field2[j].get(newObj) == null) {
                        continue;
                    }
                    //如果field1[i]属性值与field2[j]属性值内容不相同
                    if (!compareTwo(field1[i].get(oldObj), field2[j].get(newObj))) {
                        CompareResult r = new CompareResult();
                        r.setFieldName(field1[i].getName());
                        r.setOldValue(field1[i].get(oldObj));
                        r.setNewValue(field2[j].get(newObj));
                        // TODO 获取属性名称功能暴露出去
//                        ApiModelProperty apiModelProperty = field1[i].getAnnotation(ApiModelProperty.class);
//                        if (apiModelProperty != null) {
//                            r.setFieldComment(apiModelProperty.value());
//                        }

                        list.add(r);
                    }
                    break;
                }
            }
        }
        return list;
    }

    @FunctionalInterface
    protected interface InvocationCallback {

        @Nullable
        Object proceedWithLog() throws Throwable;
    }

    private Serializable getPrimaryKey(Object et) {
        // 反射获取实体类
        Class<?> clazz = et.getClass();
        // 不含有表名的实体就默认通过
        if (!clazz.isAnnotationPresent(TableName.class)) {
            return (Serializable) et;
        }
        // 获取表名
        TableName tableName = clazz.getAnnotation(TableName.class);
        String tbName = tableName.value();
        if (StringUtils.isBlank(tbName)) {
            return null;
        }
        String pkName = null;
        String pkValue = null;
        // 获取实体所有字段
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 设置些属性是可以访问的
            field.setAccessible(true);
            if (field.isAnnotationPresent(TableId.class)) {
                // 获取主键
                pkName = field.getName();
                try {
                    // 获取主键值
                    pkValue = field.get(et).toString();
                } catch (Exception e) {
                    pkValue = null;
                }

            }
        }
        return pkValue;

    }

    /**
     * 对比两个数据是否内容相同
     *
     * @param object1,object2
     * @return boolean类型
     */
    private boolean compareTwo(Object object1, Object object2) {

        if (object1 == null && object2 == null) {
            return true;
        }
        if (object1 == null && object2 != null) {
            return false;
        }
        if (object1.equals(object2)) {
            return true;
        }
        return false;
    }
}
  • 日志对象
public class LogInfo {
    /** 日志主键*/
//    @TableId(type = IdType.UUID)
    private String logId;

    /** 日志类型*/
    private String type;

    /** 日志标题*/
    private String title;

    /** 日志摘要*/
    private String description;

    /** 请求IP*/
    private String ip;

    /** URI*/
    private String requestUri;

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

    /** 提交参数*/
    private String params;

    /** 异常*/
    private String exception;

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

    /** 请求时长*/
    private Long timeout;

    /** 用户登入名*/
    private String loginName;

    /** requestID*/
    private String requestId;

    /** 历史数据*/
    private String dataSnapshot;

    /** 日志状态*/
    private Integer status;

    //setter/getter省略
 }   

MybatisPlusDataLogConfiguration启用配置

@ConditionalOnClass(BaseMapper.class)
@Configuration
public class MybatisPlusDataLogConfiguration {
    private static final Logger LOGGER = LoggerFactory.getLogger(MybatisPlusDataLogConfiguration.class);

    @Bean
    public AspectJExpressionPointcutAdvisor mybatisPlusMethodAdvisor(MybatisPlusMethodInterceptor interceptor) {
        AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
        advisor.setExpression("execution(* com.easycode8.easylog.sample.mapper.*.*(..))");
        advisor.setAdvice(interceptor);
        LOGGER.info("[easy-log]启动mybatis-plus操作数据比对");
        return advisor;
    }


    @Bean
    public MybatisPlusMethodInterceptor mybatisPlusMethodInterceptor() {
        return new MybatisPlusMethodInterceptor();
    }
}

运行效果

修改信息: 记录新旧值
在这里插入图片描述
删除信息: 记录历史数据
在这里插入图片描述

maven依赖参考

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <version>2.3.2.RELEASEversion>
        dependency>
        
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.4.2version>
            <optional>trueoptional>
        dependency>

        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
            <version>3.8.1version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.79version>
        dependency>

总结

记录操作前数据,可以有多种实现做法,mybatis的拦截器也可以实现类似逻辑.

public class RecordInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        String sql = boundSql.getSql();
        SqlCommandType sqlCommandType = ms.getSqlCommandType();

        if (sqlCommandType == SqlCommandType.DELETE || sqlCommandType == SqlCommandType.UPDATE) {
            // 记录信息的逻辑
            String tableName = getTableNameFromSql(sql);
            // 获取删除或修改前的记录信息
            List<Map<String, Object>> originalRecords = getOriginalRecords(tableName, parameterObject);
            // 将信息记录到日志文件或数据库中
            recordLog(tableName, originalRecords);
        }

        return invocation.proceed();
    }

    private String getTableNameFromSql(String sql) {
        // 根据 SQL 语句获取表名
        // ...
    }

    private List<Map<String, Object>> getOriginalRecords(String tableName, Object parameterObject) {
        // 获取删除或修改前的记录信息
        // ...
    }

    private void recordLog(String tableName, List<Map<String, Object>> originalRecords) {
        // 将信息记录到日志文件或数据库中
        // ...
    }
}

你可能感兴趣的:(mybatis,spring,java)