数据追踪插件:实现高效数据变更记录与管理的利器

数据追踪插件:实现高效数据变更记录与管理的利器

引言

在现代企业级应用开发中,数据变更记录(Data Change Tracking)是一个非常重要的功能。它不仅能帮助我们追踪数据的变更历史,还能在数据审计、问题排查、数据恢复等场景中发挥关键作用。然而,实现一个高效、灵活且易于扩展的数据追踪系统并非易事。本文将介绍一个基于Spring Boot和MyBatis-Plus的数据追踪插件,该插件通过注解驱动的方式,实现了对数据变更的自动记录与管理。

插件概述

本插件的核心目标是通过注解的方式,自动捕获数据变更,并将变更内容记录到指定的存储中。插件的主要功能包括:

  1. 自动捕获数据变更:通过拦截器自动捕获数据库的增、删、改操作。
  2. 注解驱动:通过自定义注解(如@DataTracer@DataTracerLabel等)标记需要追踪的字段或类。
  3. 灵活扩展:支持通过接口扩展字典转换、数据记录等功能的实现。
  4. 高效缓存:通过缓存机制减少重复计算,提升性能。

核心设计

注解驱动

插件通过一系列自定义注解来标记需要追踪的字段或类。以下是插件中定义的主要注解:

  • @DataTracer:标记需要追踪的类。
  • @DataTracerLabel:标记字段的描述信息。
  • @DataTracerIgnore:标记不需要追踪的字段。
  • @DataTracerEnum:标记枚举类型的字段,支持枚举值的转换。
  • @DataTracerDict:标记字典类型的字段,支持字典值的转换。
  • @DataTracerSql:支持通过SQL查询关联字段的显示值。

拦截器机制

插件通过DataTracerInterceptor拦截器捕获数据库的增、删、改操作。拦截器继承自MyBatis-Plus的DataChangeRecorderInnerInterceptor,并在其基础上扩展了数据变更记录的功能。

java 复制代码
public class DataTracerInterceptor extends DataChangeRecorderInnerInterceptor {
    private static final String SEPARATOR_BR = "<br/>";

    private static final String UPDATE_TEMPLATE = "由【%s】变更为【%s】";

    private final DataTracerHandler dataTracerHandler;

    public DataTracerInterceptor(String mapperPackage) {
        dataTracerHandler = new DataTracerHandler(mapperPackage);
    }

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        // 获取mapper类
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();

        // 检查当前Mapper方法是否需要跳过数据跟踪
        if (dataTracerHandler.invalid(ms.getId())) {
            return;
        }
        // 根据注解配置判断是否要忽略数据跟踪
        if (InterceptorIgnoreHelper.willIgnoreOthersByKey(ms.getId(), DataTracerConstant.DATA_TRACER_IGNORE_KEY)) {
            return;
        }

        super.beforePrepare(sh, connection, transactionTimeout);
    }

    /**
     * 处理数据变化记录内部拦截器捕获的操作结果
     * 该方法根据操作类型(插入、删除、更新)调用相应的方法来处理数据变化
     *
     * @param operationResult 数据更新结果
     */
    @Override
    protected void dealOperationResult(OperationResult operationResult) {
        logger.debug("dealOperationResult: {}", operationResult);
        // 获取操作类型
        String operation = operationResult.getOperation();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(operation.toUpperCase());

        processChanges(operationResult, sqlCommandType);
    }

    // 新增一个处理变更数据的通用方法
    private void processChanges(OperationResult result, SqlCommandType operationType) {
        TableInfo tableInfo = TableInfoHelper.getTableInfo(result.getTableName());
        Class<?> entityType = tableInfo.getEntityType();
        List<Field> fieldList = dataTracerHandler.getFieldList(entityType);
        List<JSONObject> changeList = JSONUtil.toList(result.getChangedData(), JSONObject.class);
        List<DataTracerContent> tracerList = new ArrayList<>();

        for (JSONObject changeInfo : changeList) {
            DataTracerContent content = buildDataTracerContent(result);
            StringJoiner joiner = new StringJoiner(SEPARATOR_BR);
            StringJoiner oldJoiner = new StringJoiner(SEPARATOR_BR);
            StringJoiner newJoiner = new StringJoiner(SEPARATOR_BR);

            for (Map.Entry<String, Object> entry : changeInfo.entrySet()) {
                handleField(entry, tableInfo, fieldList, content, joiner, oldJoiner, newJoiner, operationType);
            }

            // 根据操作类型设置不同内容
            if (SqlCommandType.UPDATE.equals(operationType)) {
                content.setDiffOld(oldJoiner.toString()).setDiffNew(newJoiner.toString()).setContent(joiner.toString());
            } else {
                content.setContent(joiner.toString());
            }

            if (!StrUtil.isBlank(content.getContent())) {
                tracerList.add(content);
            }
        }

        // 处理数据追踪内容
        processDataTracerContent(tracerList);
    }

    // 字段处理统一方法
    private void handleField(Map.Entry<String, Object> entry,
                             TableInfo tableInfo,
                             List<Field> fieldList,
                             DataTracerContent content,
                             StringJoiner joiner,
                             StringJoiner oldJoiner,
                             StringJoiner newJoiner,
                             SqlCommandType operationType) {
        String fieldName = StrUtil.toCamelCase(entry.getKey().toLowerCase());
        String changeContent = entry.getValue().toString();

        // 处理主键
        if (dataTracerHandler.isPrimaryKey(fieldName, tableInfo.getKeyProperty(), tableInfo.getKeyColumn())) {
            content.setDataId(changeContent.replaceAll("null->", ""));
            return;
        }

        // 处理字段元数据
        Field field = dataTracerHandler.getField(fieldList, fieldName);
        if (Objects.nonNull(field) && dataTracerHandler.ignoreDataTracer(field)) {
            return;
        }

        // 获取字段描述
        String fieldDesc = (field != null) ? dataTracerHandler.getFieldDesc(field) : fieldName;

        // 根据不同操作类型处理值
        String[] values = changeContent.split("->");
        String oldValue = values[0];
        String newValue = (values.length > 1) ? values[1] : "";

        if (field != null) {
            oldValue = dataTracerHandler.getFieldValue(field, oldValue);
            newValue = dataTracerHandler.getFieldValue(field, newValue);
        }

        // 构建输出内容
        switch (operationType) {
            case INSERT:
                joiner.add(fieldDesc + ":" + newValue.replaceAll("null->", ""));
                break;
            case UPDATE:
                oldJoiner.add(fieldDesc + ":" + oldValue);
                newJoiner.add(fieldDesc + ":" + newValue);
                joiner.add(fieldDesc + ":" + String.format(UPDATE_TEMPLATE, oldValue, newValue));
                break;
            case DELETE:
                joiner.add(fieldDesc + ":" + oldValue);
                break;
        }
    }

    /**
     * 根据操作结果获取追踪内容
     */
    private DataTracerContent buildDataTracerContent(OperationResult operationResult) {
        // 创建一个DataTracerContent对象用于存储追踪信息
        DataTracerContent content = new DataTracerContent();
        // 设置数据名称为操作结果中的表名
        content.setDataName(operationResult.getTableName());
        // 设置类型为操作结果中的操作类型
        content.setType(operationResult.getOperation());
        // 返回填充好的DataTracerContent对象
        return content;
    }

    /**
     * 处理数据追踪内容列表
     * <p>
     * 该方法将接收到的数据追踪内容列表传递给数据追踪记录服务进行记录
     * 主要目的是为了存储或进一步处理这些数据追踪信息
     *
     * @param tracerList 数据追踪内容列表,包含了多个数据追踪对象
     */
    protected void processDataTracerContent(List<DataTracerContent> tracerList) {
        if (tracerList == null || tracerList.isEmpty()) {
            logger.debug("数据追踪内容列表为空,不进行操作");
            return;
        }

        SpringUtil.getBean(IDataTracerManager.class).recordDataTracer(tracerList);
    }

    /**
     * 数据记录 实体
     *
     * @author Junerliy
     */
    @Accessors(chain = true)
    @Data
    public static class DataTracerContent {
        /**
         * 数据id
         */
        private Object dataId;
        /**
         * 数据表名
         */
        private String dataName;
        /**
         * 业务操作类型
         */
        private String type;
        /**
         * 内容
         */
        private String content;
        /**
         * diff 差异:旧的数据
         */
        private String diffOld;
        /**
         * 差异:新的数据
         */
        private String diffNew;
        /**
         * 扩展字段
         */
        private String extraData;
    }
}

数据处理与缓存

DataTracerHandler是插件的核心处理器,负责处理字段的注解信息、获取字段的描述、处理字段的值转换等。为了提升性能,插件使用了多级缓存机制,包括字段缓存、枚举值缓存、字段描述缓存等。

java 复制代码
public class DataTracerHandler {
    /**
     * 方法或类(名称) 与 注解的映射关系缓存
     */
    private final Map<String, DataTracer> dataTracerCacheMap = new ConcurrentHashMap<>();
    /**
     * 枚举类与枚举值(code,desc)的映射缓存
     */
    private final Map<String, Map<Object, String>> enumValueCacheMap = new ConcurrentHashMap<>();
    /**
     * 类 加注解字段缓存
     */
    private final Map<Class<?>, List<Field>> fieldCacheMap = new ConcurrentHashMap<>();
    /**
     * 类 加注解字段缓存
     */
    private final Map<String, DataTracerIgnore> fieldIgnoreCacheMap = new ConcurrentHashMap<>();
    /**
     * 字段描述缓存
     */
    private final Map<String, String> fieldDescCacheMap = new ConcurrentHashMap<>();

    public DataTracerHandler(String mapperPackage) {
        scanMapperClasses(mapperPackage);
    }

    /**
     * 通过 mapperPackage 设置的扫描包 扫描缓存有注解的方法与类
     */
    private void scanMapperClasses(String mapperPackage) {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
        String[] packagePatternArray = StrUtil.splitToArray(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

        String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
        try {
            for (String packagePattern : packagePatternArray) {
                String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
                Resource[] resources = resolver.getResources(classpath + path + "/*.class");
                for (Resource resource : resources) {
                    ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
                    Class<?> clazz = Resources.classForName(classMetadata.getClassName());
                    // 获取类注解
                    if (AnnotationUtil.hasAnnotation(clazz, DataTracer.class)) {
                        DataTracer dataTracer = AnnotationUtil.getAnnotation(clazz, DataTracer.class);
                        dataTracerCacheMap.put(clazz.getName(), dataTracer);
                    }
                }
            }
        } catch (Exception e) {
            log.error("初始化数据变更缓存时出错:{}", e.getMessage());
        }
    }

    public DataTracer getDataTracer(String mapperId) {
        String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
        return dataTracerCacheMap.get(clazzName);
    }

    /**
     * 是否无效
     */
    public boolean invalid(String mapperId) {
        return getDataTracer(mapperId) == null;
    }

    public List<Field> getFieldList(Class<?> tClass) {
        // 从缓存中查询
        List<Field> fieldList = fieldCacheMap.get(tClass);
        if (null != fieldList) {
            return fieldList;
        }

        // 这一段递归代码 是为了 从父类中获取属性
        Class<?> tempClass = tClass;
        fieldList = new ArrayList<>();
        while (tempClass != null) {
            Field[] declaredFields = tempClass.getDeclaredFields();
            for (Field field : declaredFields) {
                // 过虑出有TableField注解标注数据库不存在字段
                TableField tableField = field.getAnnotation(TableField.class);
                if (Objects.nonNull(tableField) && !tableField.exist()) {
                    continue;
                }
                field.setAccessible(true);
                fieldList.add(field);
            }
            tempClass = tempClass.getSuperclass();
        }

        fieldCacheMap.put(tClass, fieldList);
        return fieldList;
    }

    public Field getField(List<Field> fieldList, String fieldName) {
        Optional<Field> optional = fieldList.stream().filter(field -> field.getName().equalsIgnoreCase(fieldName)).findFirst();
        return optional.orElse(null);
    }

    /**
     * 获取字段描述信息
     */
    public String getFieldDesc(Field field) {
        // 根据字段名称 从缓存中查询
        String fieldName = field.toGenericString();
        String desc = fieldDescCacheMap.get(fieldName);
        if (null != desc) {
            return desc;
        }

        DataTracerLabel dataTracerLabel = field.getAnnotation(DataTracerLabel.class);
        if (dataTracerLabel != null) {
            desc = dataTracerLabel.value();
            fieldDescCacheMap.put(fieldName, desc);
            return desc;
        }

        return field.getName();
    }

    /**
     * 获取字段值
     */
    public String getFieldValue(Field field, String fieldValue) {
        if (StrUtil.equalsAnyIgnoreCase(fieldValue, StringPool.NULL)) {
            return fieldValue;
        }

        DataTracerSql dataTracerSql = field.getAnnotation(DataTracerSql.class);
        DataTracerEnum dataTracerEnum = field.getAnnotation(DataTracerEnum.class);
        DataTracerDict dataTracerDict = field.getAnnotation(DataTracerDict.class);
        DataTracerBigDecimal dataTracerBigDecimal = field.getAnnotation(DataTracerBigDecimal.class);
        if (dataTracerEnum != null) {
            return this.getEnumDisplayValue(dataTracerEnum, fieldValue);
        } else if (dataTracerDict != null) {
            return SpringUtil.getBean(IDataTracerDictConvert.class).convertDictName(dataTracerDict.dictType(), fieldValue);
        } else if (dataTracerSql != null) {
            return this.getRelateDisplayValue(fieldValue, dataTracerSql);
        } else if (dataTracerBigDecimal != null) {
            return new BigDecimal(fieldValue).setScale(dataTracerBigDecimal.scale(), RoundingMode.HALF_UP).toString();
        } else if (field.getType().equals(Date.class)) {
            return DateUtil.formatDateTime(DateUtil.parse(fieldValue));
        } else if (field.getType().equals(LocalDateTime.class)) {
            return LocalDateTimeUtil.formatNormal(LocalDateTimeUtil.parse(fieldValue));
        } else if (field.getType().equals(LocalDate.class)) {
            return LocalDateTimeUtil.formatNormal(LocalDateTimeUtil.parseDate(fieldValue));
        }

        return fieldValue;
    }

    private String getEnumDisplayValue(DataTracerEnum anno, String fieldValue) {
        String className = anno.enumClass().getName();
        Map<Object, String> cacheMap = enumValueCacheMap.get(className);
        if (Objects.nonNull(cacheMap)) {
            return cacheMap.getOrDefault(fieldValue, fieldValue);
        }

        Map<Object, String> enumValueMap = new HashMap<>();
        Enum<?>[] enumConstants = anno.enumClass().getEnumConstants();
        for (Enum<?> enumConstant : enumConstants) {
            Object codeValue = invokeGetter(enumConstant, anno.codeField());
            String descValue = invokeGetter(enumConstant, anno.descField());
            enumValueMap.put(String.valueOf(codeValue), descValue);
        }

        enumValueCacheMap.put(className, enumValueMap);
        return enumValueMap.getOrDefault(fieldValue, fieldValue);
    }

    /**
     * 获取关联字段的显示值
     */
    private String getRelateDisplayValue(Object fieldValue, DataTracerSql dataTracerSql) {
        BaseMapper<?> mapper = null;
        Class<? extends BaseMapper> relateMapper = dataTracerSql.relateMapper();
        if (ObjectUtil.isNotNull(relateMapper)) {
            mapper = SpringUtil.getBean(relateMapper);
        } else {
            String mapperBeanName = dataTracerSql.relateMapperBeanName();
            mapper = SpringUtil.getBean(mapperBeanName);
        }

        if (mapper == null) {
            return "";
        }
        String relateFieldValue = fieldValue.toString();
        QueryWrapper qw = new QueryWrapper();
        qw.select(StrUtil.toUnderlineCase(dataTracerSql.relateDisplayColumn()));
        qw.in(StrUtil.toUnderlineCase(dataTracerSql.relateColumn()), relateFieldValue);
        List<Object> displayValue = mapper.selectObjs(qw);
        if (CollectionUtils.isEmpty(displayValue)) {
            return "";
        }

        return StrUtil.join(",", displayValue);
    }


    private <E> E invokeGetter(Object obj, String propertyName) {
        Object object = obj;
        for (String name : StrUtil.split(propertyName, ".")) {
            String getterMethodName = "get" + StringUtils.capitalize(name);
            object = ReflectUtil.invoke(object, getterMethodName);
        }
        return (E) object;
    }

    public boolean ignoreDataTracer(Field field) {
        // 根据字段名称 从缓存中查询
        String fieldName = field.toGenericString();
        DataTracerIgnore ignoreCache = fieldIgnoreCacheMap.get(fieldName);
        if (null != ignoreCache) {
            return true;
        }

        DataTracerIgnore dataTracerIgnore = field.getAnnotation(DataTracerIgnore.class);
        if (dataTracerIgnore != null) {
            fieldIgnoreCacheMap.put(fieldName, dataTracerIgnore);
            return true;
        }

        return false;
    }

    /**
     * 判断给定字段是否为表的主键
     *
     * @param fieldName 需要判断的字段名
     * @param strs      主键字段,包含表的相关信息,如主键属性名和主键列名
     * @return 如果字段名与表信息中的主键属性名或主键列名匹配(忽略大小写),则返回true;否则返回false
     */
    public boolean isPrimaryKey(String fieldName, CharSequence... strs) {
        return StrUtil.equalsAnyIgnoreCase(fieldName, strs);
    }

}

扩展接口

插件提供了两个核心接口IDataTracerManagerIDataTracerDictConvert,允许开发者根据实际需求扩展数据记录和字典转换的功能。

java 复制代码
public interface IDataTracerManager {
    void recordDataTracer(List<DataTracerContent> tracerList);
}

public interface IDataTracerDictConvert {
    String convertDictName(String type, String value);
}

使用示例

配置插件

首先,在Spring Boot的配置文件中启用插件,并指定需要扫描的Mapper包路径。

yaml 复制代码
datatracer:
  mapper-package: com.example.mapper

标记需要追踪的类与字段

在实体类中使用@DataTracer注解标记需要追踪的类,并使用其他注解标记需要追踪的字段。

java 复制代码
public class User {
    @DataTracerLabel("用户ID")
    private Long id;

    @DataTracerLabel("用户名")
    private String username;

    @DataTracerEnum(enumClass = GenderEnum.class, codeField = "code", descField = "desc")
    private String gender;

    @DataTracerDict(dictType = "user_status")
    private String status;

    @DataTracerIgnore
    private String password;
  
    @DataTracerSql(relateColumn = "dept_id", relateDisplayColumn = "dept_name", relateMapper = DeptMapper.class)
    private Long deptId;
}
java 复制代码
@DataTracer
public interface UserMapper extends BaseMapper<User> {}

自定义数据记录与字典转换

实现IDataTracerManagerIDataTracerDictConvert接口,自定义数据记录和字典转换的逻辑。

java 复制代码
@Service
public class CustomDataTracerManager implements IDataTracerManager {
    @Override
    public void recordDataTracer(List<DataTracerContent> tracerList) {
        // 自定义数据记录逻辑
    }
}

@Service
public class CustomDataTracerDictConvert implements IDataTracerDictConvert {
    @Override
    public String convertDictName(String type, String value) {
        // 自定义字典转换逻辑
    }
}

总结

​ 本文介绍了一个基于Spring Boot和MyBatis-Plus的数据追踪插件,该插件通过注解驱动的方式,实现了对数据变更的自动记录与管理。插件的核心设计包括注解驱动、拦截器机制、数据处理与缓存、扩展接口等。通过该插件,开发者可以轻松实现数据变更记录功能,并根据实际需求灵活扩展。 ​ 该插件不仅提高了数据变更记录的效率,还通过缓存机制提升了性能,适用于各种企业级应用场景。希望本文能为开发者提供一些有价值的参考,助力大家在数据追踪领域的探索与实践。

相关推荐
半城抹茶15 小时前
关于更新字段为空值——MybatisPlus框架
java·mybatis
雾喔17 小时前
Java的缓存
java·缓存·mybatis
等什么君!18 小时前
Mybatis缓存机制(一级缓存和二级缓存)
java·缓存·mybatis
Warren9820 小时前
MySQL DDL数据定义语句
数据库·spring boot·笔记·mysql·oracle·tomcat·mybatis
好奇的菜鸟1 天前
在Spring Boot + MyBatis中优雅处理多表数据清洗:基于XML的配置化方案
xml·spring boot·mybatis
岁岁岁平安1 天前
SpringMVC学习(controller层加载控制与(业务、功能)bean加载控制、Web容器初始化配置类)(3)
java·学习·spring·mybatis·springmvc
2401_853275732 天前
能简述一下动态 SQL 的执行原理吗
java·sql·mybatis
neeef_se2 天前
IDEA搭建SpringBoot,MyBatis,Mysql工程项目
spring boot·intellij-idea·mybatis
阴阳怪气乌托邦2 天前
MyBatis与其使用方法讲解
mybatis