需求原因

更新属于敏感操作——相较于删除(目前绝大部分项目应当都是使用的逻辑删除),更新后的数据会覆盖掉之前的数据,因此需要格外关注,日志中记录下更新操作前后的数据变化有利于之后对于数据的掌握和补救恢复

要求

日志表关于更新操作需要特别记录的三个点:更新前的数据、更新后的数据、更新过程中跟踪列数据的变化

实现

前提信息

相关表、字段、数据
sys001

系统表:记录所有表字段信息、状态、类型等

s001_name_short

表名简称:记录各表名的简称(数据安全考虑)

s001_fields

表字段信息:记录各表各字段信息:如是否显示、是否跟踪、是否已被删除等

sys_user

若依系统表:用户信息

sys102

语言表:基本功能已完成用来测试的表

sys310

日志表:用来记录操作日志

s310_var_content

内容:记录更新前的数据信息

s310_var_content_new

新内容:记录更新后的数据信息

s310_var_content

跟踪列信息:记录更新前后跟踪列变化的信息
src\main\java\com\ruoyi\framework\web\service\UserDetailsServiceImpl.java

涉及到的技术(思想)

AOP

如果每个对数据的操作方法都调用一下日志记录方法,繁琐是一方面,更麻烦的是如果后续需求修改则需要更多的时间、人力;
这边使用AOP的思想进行统一处理,调用了更新方法我就记录一下更新前后的数据变化,虽然实现难度远大于(于我个人目前而言)每次操作数据库时调用日志记录方法,但对于之后的维护或者需求修改方面都更为友好
这里主要使用到@Before和@After,操作前记录一下数据库中将要更新的数据信息;操作后记录一下数据库中呗更新的数据信息,并对两者进行对比

注解

说到AOP大多数情况都会使用到注解,自定义一个注解,在更新方法上引用一下这个注解,AOP就能很方便地获取到被调用的方法,并对其进行增强处理

反射

异步操作调用方法类和方法当然需要用到反射机制

数据处理(字符串)

虽然对数据字符串进行处理会比较烦乱,但是在一定程度上可以避免反射带来的异常可能,能够直接进行字符串截取处理的信息就省的进行反射处理了

上手编程

自定义注解

src\main\java\com\ruoyi\common\annotation\Update.java
实际上内部的参数都没有用到(至少暂时没有),直接从若依原本的自定义注解拷贝来的

import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.enums.OperatorType;
import java.lang.annotation.*;

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Update {
    /**
     * 模块
     */
    public String title() default "";

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

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

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

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

切面(AOP)

src\main\java\com\ruoyi\framework\aspectj\Sys310Aspect.java

import com.ruoyi.common.annotation.Update;
import com.ruoyi.common.utils.sql.StringToListUtil;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.domain.Sys310;
import com.ruoyi.system.domain.vo.UpdateVo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;

@Aspect
@Component
public class Sys310Aspect {
    private Map<String, Object> oldObject;
    private Map<String, Object> newObject;
    
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 处理请求前执行
     *
     * @param joinPoint 切点
     */
    @Before(value = "@annotation(controllerUpdate)")
    public void doBefore(JoinPoint joinPoint, Update controllerUpdate){
        // 获取切点对象参数
        Object[] args = joinPoint.getArgs();
        // 确定有参(update必然得有参数)
        if (args.length > 0) {
            // 获取类
            Class<?> aClass = args[0].getClass();
            // 参数对象转为字符串——为之后的截取
            String objectString = args[0].toString();
            // 获取全类名
            String className = aClass.getName();
            // 通过.进行分割,取最后的类名
            String[] strings = className.split("\\.");
            className = strings[strings.length - 1];
            // 将对象类名处理为表名、表名简称
            String tableName = className.toLowerCase();
            String simpleName = "";
            String tableId = "";
            // 测试的是若依自带的SysUser
            if (className.contains("ser")) {
                // 自带表特殊处理
                tableName = tableName.substring(0, 3) + "_" + tableName.substring(3);
                simpleName = className.substring(3).toLowerCase();
                tableId = objectString.substring(objectString.indexOf(simpleName + "Id") + (simpleName + "Id").length()).substring(1, objectString.substring(objectString.indexOf(simpleName + "Id") + (simpleName + "Id").length()).indexOf("\n") - 1);
            } else if (className.contains("Sys")) { // 项目实际使用的表、JavaBean类名格式
                // sys表统一处理
                simpleName = "s" + className.substring(3).toLowerCase();
                // id截取 —— 后续细化
                // 截取到"表id="后的数据
                String obj0 = objectString.substring(objectString.indexOf(simpleName + "Id") + (simpleName + "Id").length() + 1);
                System.out.println(obj0);
                // 截取到第一个","前的数据——即表id
                String obj1 = obj0.substring(0, obj0.indexOf(","));
                System.out.println(obj1);
                tableId = obj1;
            }
            // 打印出表名、表名简称、表id值
            System.out.println(tableName + ":" + simpleName + ":" + tableId);
            System.out.println("------------打印查出的对象----------------");
            // 新建updateVo保存以上信息
            UpdateVo updateVo = new UpdateVo(tableName, simpleName, tableId);
            // 异步:查询修改前的对象信息
            oldObject = AsyncFactory.selectOneById(updateVo);
        }
    }

	/**
     * 处理请求后执行
     *
     * @param joinPoint 切点
     */
    @After(value = "@annotation(controllerUpdate)")
    public void doAfter(JoinPoint joinPoint, Update controllerUpdate)
    {
        // 获取切点对象参数
        Object[] args = joinPoint.getArgs();
        // 确定有参(update必然得有参数)
        if (args.length > 0) {
            // 获取类
            Class<?> aClass = args[0].getClass();
            // 参数对象转为字符串——为之后的截取
            String objectString = args[0].toString();
            // 获取全类名
            String className = aClass.getName();
            // 通过.进行分割,取最后的类名
            String[] strings = className.split("\\.");
            className = strings[strings.length - 1];
            // 将对象类名处理为表名、表名简称
            String tableName = className.toLowerCase();
            String simpleName = "";
            String tableId = "";
            // 测试的是若依自带的SysUser
            // 测试的是若依自带的SysUser
            if (className.contains("ser")) {
                // 自带表特殊处理
                tableName = tableName.substring(0, 3) + "_" + tableName.substring(3);
                simpleName = className.substring(3).toLowerCase();
                tableId = objectString.substring(objectString.indexOf(simpleName + "Id") + (simpleName + "Id").length()).substring(1, objectString.substring(objectString.indexOf(simpleName + "Id") + (simpleName + "Id").length()).indexOf("\n") - 1);
            } else if (className.contains("Sys")) { // 项目实际使用的表、JavaBean类名格式
                // sys表统一处理
                simpleName = "s" + className.substring(3).toLowerCase();
                // id截取 —— 后续细化
                // 截取到"表id="后的数据
                String obj0 = objectString.substring(objectString.indexOf(simpleName + "Id") + (simpleName + "Id").length() + 1);
                System.out.println(obj0);
                // 截取到第一个","前的数据——即表id
                String obj1 = obj0.substring(0, obj0.indexOf(","));
                System.out.println(obj1);
                tableId = obj1;
            }
            // 打印出表名、表名简称、表id值
            System.out.println(tableName + ":" + simpleName + ":" + tableId);
            System.out.println("------------打印查出的对象----------------");
            // 新建updateVo保存以上信息
            UpdateVo updateVo = new UpdateVo(tableName, simpleName, tableId);
            // 异步:查询修改后的对象信息
            newObject = AsyncFactory.selectOneById(updateVo);
            // 新建sys001变量保存Sys001对象信息
            String fileds = null;
            // 尝试从redis中获取s001表的对应表字段s001_fields信息
            fileds = redisTemplate.opsForValue().get("s001:" + simpleName).toString();
            // redis中没有则从s001表查询对应表字段s001_fields信息
            if (fileds == null) {
                fileds = AsyncFactory.selectSys001BySimpleName(simpleName);
            }
            // 分析出对应表各字段中flag_security="Y"的field_name_en信息
            List<Map<String, String>> maps = StringToListUtil.toJson(fileds);
            List<String> fieldList = new ArrayList<>();
            for (Map<String, String> map : maps) {
                for (Map.Entry<String, String> entry : map.entrySet()) {
                    System.out.println(entry.getKey() + ":" + entry.getValue());
                }
                if ("\"Y\"".equals(map.get("flag_security"))) {
                    fieldList.add(map.get("field_name_en").replaceAll("\"", ""));
                }
            }
            StringBuilder updateMessage = new StringBuilder();
            for (String field : fieldList) {
                if (!oldObject.get(field).equals(newObject.get(field))) {
                    updateMessage.append(field + ":" + oldObject.get(field) + "——>" + newObject.get(field)+";");
                }
            }
            Sys310 sys310 = new Sys310();
            sys310.setS310VarContent(oldObject.toString());
            sys310.setS310VarContentNew(newObject.toString());
            sys310.setS310VarDescDetail(updateMessage.toString().length() != 0 ? updateMessage.toString() : "没有修改跟踪列内容");
            // 保存到数据库——sys310
            AsyncManager.me().execute(AsyncFactory.recordSys310(sys310));
        }
    }
}

工具类StringToListUtil

src\main\java\com\ruoyi\common\utils\sql\StringToListUtil.java

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class StringToListUtil {
    // 将传入的s001需处理为json的字符串字段数据转化为json
    public static List<Map<String, String>> toJson(String s) {
        // 创建一个map数组用来保存转换好的数据
        List<Map<String, String>> mapList = new ArrayList<>();
        // 去掉字符串开头的“[{”和结尾的“}]”
        s = s.substring(2, s.length() - 3);
        // 通过“},{”分割出各个对象
        String[] ss = s.split("},\\{");
        // 处理各个对象
        for (String s1 : ss) {
            // 通过“,”分割出各个对象的各个元素键值
            String[] split = s1.split(",");
            Map<String, String> map = new HashMap<>();
            // 处理各个元素键值
            for (int i = 0; i < split.length; i++) {
                // 去掉开头的“"”,方便之后通过定位“"”来分割属性名和属性值
                split[i] = split[i].substring(1);
                // 通过“"”和“:”分割出属性名、属性值
                try {
                    map.put(split[i].substring(0, split[i].indexOf("\"")), split[i].substring(split[i].indexOf(":") + 1));
                } catch (Exception e) {
//                    e.printStackTrace();
                    System.out.println(e.getMessage());
                }
            }
            // 将当前对象保存到对象数组mapList(所有信息)
            mapList.add(map);
        }
        // 返回
        return mapList;
    }

    // 将传入的格式为{"sql":null,"layout":[]}的
    // s001需处理为json的字符串字段数据转化为json
    public static List<Map<String, String>> toJsonHaveSql(String s) {
        s = s.substring(21, s.length() - 1);
        // 创建一个map数组用来保存转换好的数据
        List<Map<String, String>> mapList = new ArrayList<>();
        // 去掉字符串开头的“[{”和结尾的“}]”
        s = s.substring(2, s.length() - 3);
        // 通过“},{”分割出各个对象
        String[] ss = s.split("},\\{");
        // 处理各个对象
        for (String s1 : ss) {
            // 通过“,”分割出各个对象的各个元素键值
            String[] split = s1.split(",");
            Map<String, String> map = new HashMap<>();
            // 处理各个元素键值
            for (int i = 0; i < split.length; i++) {
                // 去掉开头的“"”,方便之后通过定位“"”来分割属性名和属性值
                split[i] = split[i].substring(1);
                // 通过“"”和“:”分割出属性名、属性值
                map.put(split[i].substring(0, split[i].indexOf("\"")), split[i].substring(split[i].indexOf(":") + 1));
            }
            // 将当前对象保存到对象数组mapList(所有信息)
            mapList.add(map);
        }
        // 返回
        return mapList;
    }
}

AsyncFactory

src\main\java\com\ruoyi\framework\manager\factory\AsyncFactory.java
原本就有的类,减少篇幅只展示新增的内容

public static Map<String, Object> selectOneById(final UpdateVo updateVo)
    {
                // 远程查询操作地点
                Map<String, Object> stringObjectMap = SpringUtils.getBean(Sys001Mapper.class).selectOneById(updateVo);
                System.out.println("----打印信息----");
                for (Map.Entry<String, Object> entry : stringObjectMap.entrySet()) {
                    System.out.println(entry.getKey()+":"+entry.getValue());
                }
        return stringObjectMap;
    }

    public static TimerTask recordSys310(final Sys310 sys310) {
        return new TimerTask()
        {
            @Override
            public void run()
            {
                // 远程查询操作地点7
                SpringUtils.getBean(Sys310Mapper.class).insertSys310(sys310);
            }
        };
    }

    public static String selectSys001BySimpleName(final String simpleName) {
        // 查询并返回Sys001
        return SpringUtils.getBean(Sys001Mapper.class).getS001FieldsByShortName(simpleName);
    }

切点位置

在这里插入图片描述

调用的方法

Sys001Mapper
在这里插入图片描述
Sys310Mapper
在这里插入图片描述
Sys310Mapper.xml
在这里插入图片描述

涉及到的JavaBean(部分属性内容)

Sys001
**
 * 
 * @TableName sys001
 */
@TableName(value ="sys001")
@Data
public class Sys001 extends BaseEntity {
    
    /**
     * 表简称
     */
    private String s001NameShort;

    /**
     * 表名
     */
    private String s001NameEn;

    @TableField(exist = false)
    private List<Map<String, String>> filesMaps;

}
Sys102
/**
 *
 * @TableName sys102
 */
@TableName(value ="sys102")
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Sys102  extends BaseEntity {
	/**
     * id
     */
    @TableId
    private String s102Id = IdAutoUtil.getId("snowflake");
   
    /**
     * 模块
     */
    private String s102VarModel;

    /**
     * 语种
     */
    private String s102VarLang;

    /**
     * key
     */
    private String s102VarKey;

    /**
     * 描述
     */
    private String s102VarDesc;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;

    public Sys102(String s102Id) {
        this.s102Id = s102Id;
    }
}
Sys310
/**
 * 
 * @TableName sys310
 */
@TableName(value ="sys310")
@Data
public class Sys310 extends BaseEntity {
    /**
     * 摘要
     */
    private String s310VarContent;

    /**
     * 详情
     */
    private String s310VarDescDetail;

    /**
     * 修改后内容
     */
    private String s310VarContentNew;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}
UpdateVo
//@Data
@ToString
@AllArgsConstructor
// extends BaseEntity
public class UpdateVo extends BaseEntity {
    private String tableName;
    private String simpleName;
    private String tableId;

    public String getTableName() {
        return tableName;
    }

    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    public String getSimpleName() {
        return simpleName;
    }

    public void setSimpleName(String simpleName) {
        this.simpleName = simpleName;
    }

    public String getTableId() {
        return tableId;
    }

    public void setTableId(String tableId) {
        this.tableId = tableId;
    }
}

展示

在这里插入图片描述

遇到的问题

怎么通过一个AOP去获取所有的更新操作前后的表数据信息

解决方案、想法

通用查询

不管是更新前还是更新后,获取对应数据都是使用的同一个方法;
另外需要解决一个AOP能够查询所有表数据的问题则需要用到通用的方法了

选择(1)

通过反射机制动态获取调用更新方法时传入的参数,以此获取到对应的JavaBean类、方法类、方法等信息
这种方法也尝试过,中间出现太多的问题严重影响我对于该功能实现的信心,最后终于还是选择了另一种于我而言更为简单的方法

选择(2)

只编写一个方法,通过动态sql的方式做到能够查询所有表数据的功能
这种方式需要解决的问题:截取字符串、sql拼接、防止sql注入

Logo

快速构建 Web 应用程序

更多推荐