基于SpringBoot + AOP + 自定义注解 实现无侵入式数据变更追踪

207 阅读11分钟

SpringBoot + AOP + 注解:实现无侵入式数据变更追踪

前言

在日常开发中,数据变更追踪(如操作日志、数据版本记录)是高频需求 —— 比如用户信息修改、订单状态变更、配置项调整等场景,都需要记录 “谁改了什么、改之前是什么、改之后是什么、什么时候改的”。传统方式是在业务代码中手动编写日志记录逻辑,不仅冗余繁琐,还容易遗漏或出错,严重违反 “单一职责原则”。

本文基于 SpringBoot + AOP + 自定义注解,提供一套无侵入式数据变更追踪方案:无需修改业务代码,仅通过注解即可自动记录变更日志,支持字段级对比、异步存储、敏感数据脱敏、字段忽略等实用功能,开箱即用。

一、核心技术选型

  • 基础框架:SpringBoot 2.x/3.x
  • 切面编程:Spring AOP(无需额外引入,SpringBoot Starter 自带)
  • 序列化:Jackson(处理对象转 JSON 对比)
  • ORM 框架:Spring Data JPA(也可替换为 MyBatis,下文附适配方案)
  • 数据库:MySQL(存储变更日志)
  • 核心思想:通过自定义注解标记目标方法,AOP 拦截方法执行,对比方法入参 / 返回值与数据库原始数据的差异,自动生成变更日志。

二、核心代码

1. 第一步:引入依赖(Maven/Gradle)

Maven 依赖
<!-- SpringBoot核心依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.15</version> <!-- 3.x版本可替换为3.1.4 -->
    <relativePath/>
</parent>

<!-- 核心依赖 -->
<<dependencies>
    <!-- SpringBoot Web(按需引入) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring AOP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- Jackson(序列化) -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    <!-- Lombok(简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</</dependencies>
Gradle 依赖
plugins {
    id 'org.springframework.boot' version '2.7.15'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'java'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'com.fasterxml.jackson.core:jackson-databind'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

2. 第二步:定义核心注解

2.1 变更追踪主注解(@DataChangeTrack)

标记需要追踪的业务方法,支持配置业务类型、业务 ID、忽略字段等。

import java.lang.annotation.*;

/**
 * 数据变更追踪注解:标记需要记录变更日志的方法
 */
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface DataChangeTrack {

    /**
     * 业务类型(如:USER=用户管理、ORDER=订单管理、CONFIG=配置管理)
     */
    String businessType();

    /**
     * 业务ID字段名(从方法入参中提取,如:userId、orderNo)
     * 若入参是对象,填对象的字段名;若入参是单个值(如Long userId),填""即可
     */
    String businessIdField() default "";

    /**
     * 操作类型(默认自动判断:新增/修改/删除)
     * 也可手动指定:CREATE/UPDATE/DELETE
     */
    OperateType operateType() default OperateType.AUTO;

    /**
     * 需要忽略的字段(如:密码、创建时间等无需追踪的字段)
     */
    String[] ignoreFields() default {};

    /**
     * 是否记录变更详情(默认true,字段级对比)
     */
    boolean recordDetail() default true;

    /**
     * 操作人字段名(从方法入参中提取,若从上下文获取,可填"")
     */
    String operatorField() default "";

    // 操作类型枚举
    enum OperateType {
        AUTO, CREATE, UPDATE, DELETE
    }
}
2.2 字段忽略注解(@IgnoreChange)

用于实体类字段,标记无需追踪的字段(优先级高于 @DataChangeTrack 的 ignoreFields)。

import java.lang.annotation.*;

/**
 * 字段忽略注解:标记无需追踪变更的字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreChange {
}

3. 第三步:定义变更日志实体(DataChangeLog)

存储变更日志的核心实体,对应数据库表。

import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Map;

/**
 * 数据变更日志表
 */
@Data
@Entity
@Table(name = "t_data_change_log") // 表名可自定义
@DynamicInsert
@DynamicUpdate
@EntityListeners(AuditingEntityListener.class)
public class DataChangeLog {

    /**
     * 主键ID(自增)
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 业务类型(如:USER、ORDER、CONFIG)
     */
    @Column(name = "business_type", nullable = false)
    private String businessType;

    /**
     * 业务ID(如:用户ID、订单号)
     */
    @Column(name = "business_id", nullable = false)
    private String businessId;

    /**
     * 操作类型(CREATE/UPDATE/DELETE)
     */
    @Column(name = "operate_type", nullable = false)
    private String operateType;

    /**
     * 操作人(用户名/工号)
     */
    @Column(name = "operator")
    private String operator;

    /**
     * 变更前数据(JSON格式)
     */
    @Column(name = "before_data", columnDefinition = "TEXT")
    private String beforeData;

    /**
     * 变更后数据(JSON格式)
     */
    @Column(name = "after_data", columnDefinition = "TEXT")
    private String afterData;

    /**
     * 变更详情(字段级对比,JSON格式)
     * 示例:{"userName": {"old": "张三", "new": "李四"}, "age": {"old": 20, "new": 22}}
     */
    @Column(name = "change_detail", columnDefinition = "TEXT")
    private String changeDetail;

    /**
     * 操作时间(自动填充)
     */
    @CreatedDate
    @Column(name = "operate_time", nullable = false, updatable = false)
    private LocalDateTime operateTime;

    /**
     * 备注(可选)
     */
    @Column(name = "remark")
    private String remark;
}

4. 第四步:实现 AOP 切面(核心逻辑)

拦截 @DataChangeTrack 标记的方法,对比数据变更,生成日志。

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 数据变更追踪切面:核心逻辑实现
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class DataChangeTrackerAspect {

    private final ObjectMapper objectMapper;
    private final DataChangeLogService changeLogService;

    // 切入点:拦截所有带@DataChangeTrack注解的方法
    @Pointcut("@annotation(com.example.track.annotation.DataChangeTrack)")
    public void dataChangePointcut() {}

    // 方法执行成功后触发(AfterReturning:确保业务逻辑执行成功再记录日志)
    @AfterReturning(pointcut = "dataChangePointcut()", returning = "result")
    public void trackDataChange(JoinPoint joinPoint, Object result) {
        try {
            // 1. 获取注解信息
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            DataChangeTrack annotation = method.getAnnotation(DataChangeTrack.class);

            // 2. 提取核心参数(业务ID、操作人、操作类型)
            String businessId = extractBusinessId(joinPoint, annotation);
            String operator = extractOperator(joinPoint, annotation);
            String operateType = determineOperateType(annotation, result);

            // 3. 获取变更前后数据(beforeData:数据库原始数据;afterData:方法入参/返回值)
            Object beforeData = getOriginalData(annotation.businessType(), businessId);
            Object afterData = getAfterData(joinPoint, result, annotation.operateType());

            // 4. 生成变更详情(字段级对比)
            String changeDetail = generateChangeDetail(beforeData, afterData, annotation);

            // 5. 构建日志实体并保存(异步保存,不影响业务性能)
            DataChangeLog changeLog = buildChangeLog(annotation, businessId, operator, operateType, beforeData, afterData, changeDetail);
            changeLogService.saveAsync(changeLog);

        } catch (Exception e) {
            log.error("数据变更追踪失败", e);
            // 日志记录失败不影响业务逻辑,仅打印异常
        }
    }

    /**
     * 提取业务ID
     */
    private String extractBusinessId(JoinPoint joinPoint, DataChangeTrack annotation) {
        String businessIdField = annotation.businessIdField();
        Object[] args = joinPoint.getArgs();
        if (args.length == 0) {
            throw new IllegalArgumentException("方法无入参,无法提取业务ID");
        }

        // 若入参是单个值(如Long userId),直接返回
        if (StringUtils.isEmpty(businessIdField)) {
            return args[0].toString();
        }

        // 若入参是对象,通过反射获取businessIdField对应的字段值
        Object arg = args[0];
        Field field = ReflectionUtils.findField(arg.getClass(), businessIdField);
        if (field == null) {
            throw new IllegalArgumentException("入参对象无字段:" + businessIdField);
        }
        ReflectionUtils.makeAccessible(field);
        return Objects.toString(ReflectionUtils.getField(field, arg), "");
    }

    /**
     * 提取操作人(可扩展:从Spring Security上下文、Token中获取)
     */
    private String extractOperator(JoinPoint joinPoint, DataChangeTrack annotation) {
        String operatorField = annotation.operatorField();
        if (StringUtils.isEmpty(operatorField)) {
            // 默认从上下文获取(示例:假设已实现UserContext工具类)
            return UserContext.getCurrentUser() == null ? "匿名用户" : UserContext.getCurrentUser().getUsername();
        }

        // 从方法入参提取操作人
        Object[] args = joinPoint.getArgs();
        Object arg = args[0];
        Field field = ReflectionUtils.findField(arg.getClass(), operatorField);
        if (field == null) {
            return "未知用户";
        }
        ReflectionUtils.makeAccessible(field);
        return Objects.toString(ReflectionUtils.getField(field, arg), "未知用户");
    }

    /**
     * 确定操作类型(自动判断/手动指定)
     */
    private String determineOperateType(DataChangeTrack annotation, Object result) {
        if (annotation.operateType() != DataChangeTrack.OperateType.AUTO) {
            return annotation.operateType().name();
        }
        // 简单逻辑:result为null可能是删除,beforeData为null可能是新增,否则是修改
        // 可根据业务自定义判断逻辑(如:根据方法名包含saveOrUpdate、delete等关键词)
        return result == null ? "DELETE" : (getOriginalData(annotation.businessType(), extractBusinessId(null, annotation)) == null ? "CREATE" : "UPDATE");
    }

    /**
     * 获取数据库原始数据(需根据业务类型和业务ID查询,这里是示例逻辑)
     */
    private Object getOriginalData(String businessType, String businessId) {
        // 实际场景:根据businessType路由到对应的Repository查询
        // 示例:if ("USER".equals(businessType)) return userRepository.findById(Long.valueOf(businessId)).orElse(null);
        return null; // 替换为真实查询逻辑
    }

    /**
     * 获取变更后数据(入参/返回值)
     */
    private Object getAfterData(JoinPoint joinPoint, Object result, DataChangeTrack.OperateType operateType) {
        // 删除操作返回null,新增/修改返回入参或返回值
        if (operateType == DataChangeTrack.OperateType.DELETE) {
            return null;
        }
        return result != null ? result : joinPoint.getArgs()[0];
    }

    /**
     * 生成字段级变更详情
     */
    private String generateChangeDetail(Object beforeData, Object afterData, DataChangeTrack annotation) throws JsonProcessingException {
        if (!annotation.recordDetail()) {
            return "未记录详细变更";
        }

        // 新增/删除无需字段对比
        if (beforeData == null || afterData == null) {
            return "新增数据" + (beforeData == null ? "" : "删除数据");
        }

        // 转换为Map便于对比
        Map<String, Object> beforeMap = objectMapper.convertValue(beforeData, Map.class);
        Map<String, Object> afterMap = objectMapper.convertValue(afterData, Map.class);

        // 过滤忽略字段(注解配置 + @IgnoreChange标记)
        Set<String> ignoreFields = new HashSet<>(Arrays.asList(annotation.ignoreFields()));
        ignoreFields.addAll(getIgnoreFieldsFromAnnotation(beforeData.getClass()));

        // 对比差异
        Map<String, Map<String, Object>> changeDetail = new HashMap<>();
        for (Map.Entry<String, Object> entry : afterMap.entrySet()) {
            String fieldName = entry.getKey();
            if (ignoreFields.contains(fieldName)) {
                continue;
            }
            Object oldVal = beforeMap.get(fieldName);
            Object newVal = entry.getValue();
            // 忽略空值对比(可自定义逻辑)
            if (!Objects.equals(oldVal, newVal) && (oldVal != null || newVal != null)) {
                Map<String, Object> diff = new HashMap<>();
                diff.put("old", desensitizeField(fieldName, oldVal)); // 脱敏处理
                diff.put("new", desensitizeField(fieldName, newVal));
                changeDetail.put(fieldName, diff);
            }
        }

        return objectMapper.writeValueAsString(changeDetail);
    }

    /**
     * 获取实体类中带@IgnoreChange注解的字段
     */
    private Set<String> getIgnoreFieldsFromAnnotation(Class<?> clazz) {
        return Arrays.stream(clazz.getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(IgnoreChange.class))
                .map(Field::getName)
                .collect(Collectors.toSet());
    }

    /**
     * 敏感字段脱敏(可扩展:手机号、身份证、密码等)
     */
    private Object desensitizeField(String fieldName, Object value) {
        if (value == null) {
            return null;
        }
        String val = value.toString();
        // 示例:手机号脱敏(138****1234)
        if ("phone".equals(fieldName) && val.length() == 11) {
            return val.replaceAll("(\d{3})\d{4}(\d{4})", "$1****$2");
        }
        // 密码直接隐藏
        if ("password".equals(fieldName) || "pwd".equals(fieldName)) {
            return "******";
        }
        // 身份证脱敏(110****1234)
        if ("idCard".equals(fieldName) && val.length() == 18) {
            return val.replaceAll("(\d{3})\d{11}(\d{4})", "$1****$2");
        }
        return value;
    }

    /**
     * 构建变更日志实体
     */
    private DataChangeLog buildChangeLog(DataChangeTrack annotation, String businessId, String operator, String operateType, Object beforeData, Object afterData, String changeDetail) throws JsonProcessingException {
        DataChangeLog changeLog = new DataChangeLog();
        changeLog.setBusinessType(annotation.businessType());
        changeLog.setBusinessId(businessId);
        changeLog.setOperateType(operateType);
        changeLog.setOperator(operator);
        changeLog.setBeforeData(beforeData == null ? null : objectMapper.writeValueAsString(beforeData));
        changeLog.setAfterData(afterData == null ? null : objectMapper.writeValueAsString(afterData));
        changeLog.setChangeDetail(changeDetail);
        changeLog.setRemark("自动记录数据变更");
        return changeLog;
    }
}

5. 第五步:日志服务层(异步保存 + 批量处理)

import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 变更日志服务:异步保存+批量处理优化
 */
@Service
@RequiredArgsConstructor
public class DataChangeLogService {

    private final DataChangeLogRepository changeLogRepository;
    // 批量保存缓存(可配置容量,如100条批量提交)
    private final List<DataChangeLog> batchCache = new CopyOnWriteArrayList<>();
    private static final int BATCH_SIZE = 100;

    /**
     * 异步保存日志(不阻塞业务线程)
     */
    @Async("changeLogAsyncPool") // 指定线程池
    public void saveAsync(DataChangeLog changeLog) {
        batchCache.add(changeLog);
        // 达到批量阈值,提交保存
        if (batchCache.size() >= BATCH_SIZE) {
            synchronized (this) {
                if (batchCache.size() >= BATCH_SIZE) {
                    changeLogRepository.saveAll(batchCache);
                    batchCache.clear();
                }
            }
        }
    }

    /**
     * 定时批量保存(防止缓存堆积,如每5分钟执行一次)
     */
    @Transactional
    public void batchSave() {
        if (!batchCache.isEmpty()) {
            changeLogRepository.saveAll(batchCache);
            batchCache.clear();
        }
    }

    // 其他查询方法(如:按业务类型+时间范围查询日志)
    public List<DataChangeLog> queryByBusinessTypeAndTimeRange(String businessType, String startTime, String endTime) {
        // 实现查询逻辑(JPA/MyBatis)
        return changeLogRepository.findByBusinessTypeAndOperateTimeBetween(businessType, parseTime(startTime), parseTime(endTime));
    }

    // 时间解析工具方法(略)
    private LocalDateTime parseTime(String timeStr) {
        // 实现字符串转LocalDateTime逻辑
        return null;
    }
}

6. 第六步:Repository 层(JPA/MyBatis 适配)

JPA 实现
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface DataChangeLogRepository extends JpaRepository<DataChangeLog, Long> {

    // 按业务类型+时间范围查询
    List<DataChangeLog> findByBusinessTypeAndOperateTimeBetween(String businessType, LocalDateTime startTime, LocalDateTime endTime);
}
MyBatis 适配(可选)

若项目使用 MyBatis,替换 Repository 为 Mapper 接口 + XML 即可,核心是实现saveAllfindByBusinessTypeAndOperateTimeBetween方法,无需修改其他逻辑。

7. 第七步:配置类(异步线程池 + 定时任务)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 异步线程池+定时任务配置
 */
@Configuration
@EnableAsync // 启用异步
@EnableScheduling // 启用定时任务
@RequiredArgsConstructor
public class AsyncScheduledConfig {

    private final DataChangeLogService changeLogService;

    /**
     * 变更日志异步线程池
     */
    @Bean("changeLogAsyncPool")
    public Executor changeLogAsyncPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // 核心线程数
        executor.setMaxPoolSize(10); // 最大线程数
        executor.setQueueCapacity(1000); // 队列容量
        executor.setKeepAliveSeconds(60); // 空闲线程存活时间
        executor.setThreadNamePrefix("change-log-async-"); // 线程名前缀
        // 拒绝策略:队列满时,由调用线程执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    /**
     * 定时批量保存日志(每5分钟执行一次)
     */
    @Scheduled(cron = "0 0/5 * * * ?")
    public void scheduledBatchSaveChangeLog() {
        changeLogService.batchSave();
    }
}

三、使用示例(无侵入式)

1. 实体类(标记忽略字段)

import com.example.track.annotation.IgnoreChange;
import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@Table(name = "t_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // 业务ID

    private String userName; // 用户名(需追踪)

    private Integer age; // 年龄(需追踪)

    @IgnoreChange // 忽略密码字段
    private String password;

    @IgnoreChange // 忽略创建时间
    private LocalDateTime createTime;
}

2. 业务 Service(添加注解即可)

import com.example.track.annotation.DataChangeTrack;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    /**
     * 新增用户:自动记录CREATE类型日志
     */
    @DataChangeTrack(businessType = "USER", businessIdField = "id", operatorField = "createBy")
    public User addUser(User user) {
        return userRepository.save(user);
    }

    /**
     * 修改用户:自动记录UPDATE类型日志,忽略age字段
     */
    @DataChangeTrack(businessType = "USER", businessIdField = "id", ignoreFields = {"age"})
    public User updateUser(User user) {
        User oldUser = userRepository.findById(user.getId()).orElseThrow(() -> new RuntimeException("用户不存在"));
        oldUser.setUserName(user.getUserName());
        oldUser.setAge(user.getAge());
        return userRepository.save(oldUser);
    }

    /**
     * 删除用户:手动指定DELETE类型
     */
    @DataChangeTrack(businessType = "USER", businessType = "id", operateType = DataChangeTrack.OperateType.DELETE)
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }
}

四、性能优化与扩展建议

1. 性能优化

  • 异步保存:通过@Async将日志记录与业务逻辑解耦,避免阻塞业务线程。
  • 批量提交:设置缓存阈值(如 100 条)+ 定时任务(如 5 分钟),减少数据库 IO。
  • 字段过滤:仅追踪必要字段,忽略密码、创建时间等无需变更的字段。
  • 索引优化:给business_typebusiness_idoperate_time字段建立联合索引,提升查询效率。

2. 功能扩展

  • 敏感数据脱敏:在desensitizeField方法中扩展更多字段类型(如银行卡号、邮箱)。
  • 多数据源支持:若日志量较大,可将变更日志存储到单独的数据库(如 MongoDB),避免占用业务库资源。
  • 日志查询 UI:开发日志查询页面,支持按业务类型、时间范围、操作人筛选,展示变更详情。
  • 异常重试:给批量保存添加重试机制(如 Spring Retry),防止因数据库临时不可用导致日志丢失。

五、注意事项

  1. 业务 ID 必须唯一:确保businessId能唯一标识一条业务数据,否则无法正确查询原始数据。
  2. 实体类序列化:被追踪的实体类需支持 Jackson 序列化(避免循环引用,可使用@JsonIgnore)。
  3. 事务一致性:日志记录使用异步 + 批量,若业务事务回滚,可能会产生无效日志,需在切面中判断事务状态(可通过TransactionSynchronizationManager实现)。
  4. 大数据量场景:若日均日志量超 10 万,建议分表存储(按时间分表,如t_data_change_log_202401)。

总结

本文提供的方案通过 SpringBoot + AOP + 注解,实现了无侵入式数据变更追踪,核心优势:

  1. 低侵入:无需修改业务代码,仅通过注解标记即可生效。
  2. 高灵活:支持字段忽略、脱敏、异步、批量等自定义配置。
  3. 易复用:代码模板可直接复制到项目,仅需修改数据库查询逻辑和业务适配。

该方案适用于大多数中小规模系统的变更追踪需求,若需应对超大规模日志存储,可进一步结合 ELK 栈或时序数据库优化。