SpringBoot + AOP + 注解 实现自动数据变更追踪实战

数据变更追踪的痛点

在我们的日常开发工作中,经常会遇到这样的场景:

  • 产品经理问:"这条数据是谁什么时候修改的?"
  • 运维人员说:"系统出了问题,需要知道哪些数据发生了变更"
  • 审计要求:"需要记录所有的数据变更历史,以便合规检查"
  • 业务人员想知道:"这个订单的状态是怎么一步步变过来的"

传统的做法往往是手动在每个业务方法中添加日志记录,不仅代码冗余,还容易遗漏。今天我们就用SpringBoot + AOP + 注解的方式来解决这个问题。

解决方案思路

今天我们要解决的,就是如何用AOP实现自动化的数据变更追踪。

核心思路是:

  1. 自定义注解:标记需要追踪的方法
  2. AOP切面:拦截被标记的方法
  3. 数据对比:比较变更前后的数据差异
  4. 变更记录:自动记录变更信息

技术选型

  • SpringBoot:快速搭建应用
  • Spring AOP:面向切面编程
  • Jackson:JSON序列化/反序列化
  • JPA/Hibernate:ORM框架
  • MySQL:数据存储

核心实现思路

1. 自定义注解定义

首先定义追踪注解:

/**
 * 数据变更追踪注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataChangeTrack {
    /**
     * 业务类型
     */
    String businessType() default "";
    
    /**
     * 业务ID字段名
     */
    String businessIdField() default "id";
    
    /**
     * 实体类类型
     */
    Class<?> entityClass();
    
    /**
     * 是否记录详细变更内容
     */
    boolean trackDetail() default true;
    
    /**
     * 忽略的字段列表
     */
    String[] ignoreFields() default {};
}

2. 变更记录实体

定义变更记录的存储实体:

@Entity
@Table(name = "data_change_log")
@Data
public class DataChangeLog {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "business_type")
    private String businessType;
    
    @Column(name = "business_id")
    private String businessId;
    
    @Column(name = "operation_type")
    private String operationType; // CREATE, UPDATE, DELETE
    
    @Column(name = "table_name")
    private String tableName;
    
    @Column(name = "before_value", columnDefinition = "TEXT")
    private String beforeValue;  // 变更前的值
    
    @Column(name = "after_value", columnDefinition = "TEXT")
    private String afterValue;   // 变更后的值
    
    @Column(name = "changed_fields", columnDefinition = "TEXT")
    private String changedFields; // 变更的字段列表
    
    @Column(name = "changer_id")
    private String changerId;
    
    @Column(name = "changer_name")
    private String changerName;
    
    @Column(name = "change_time")
    private LocalDateTime changeTime;
    
    @Column(name = "remark", columnDefinition = "TEXT")
    private String remark;
}

3. AOP切面实现

创建核心的AOP切面:

@Aspect
@Component
@Slf4j
public class DataChangeTrackerAspect {
    
    @Autowired
    private DataChangeLogService logService;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    /**
     * 环绕通知,拦截被@DataChangeTrack注解标记的方法
     */
    @Around("@annotation(dataChangeTrack)")
    public Object around(ProceedingJoinPoint joinPoint, DataChangeTrack dataChangeTrack) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        log.debug("开始追踪方法: {}", methodName);
        
        // 获取操作前的数据
        Map<String, Object> beforeData = getBeforeData(joinPoint, dataChangeTrack);
        
        try {
            // 执行原方法
            Object result = joinPoint.proceed();
            
            // 获取操作后的数据
            Map<String, Object> afterData = getAfterData(joinPoint, result, dataChangeTrack);
            
            // 记录变更
            recordChange(dataChangeTrack, beforeData, afterData, result);
            
            return result;
        } catch (Exception e) {
            log.error("数据变更追踪发生异常", e);
            throw e;
        }
    }
    
    /**
     * 获取变更前的数据
     */
    private Map<String, Object> getBeforeData(ProceedingJoinPoint joinPoint, DataChangeTrack annotation) {
        try {
            Object[] args = joinPoint.getArgs();
            if (args.length == 0) {
                return Collections.emptyMap();
            }
            
            // 假设第一个参数是要操作的实体对象
            Object entity = args[0];
            if (entity == null) {
                return Collections.emptyMap();
            }
            
            // 序列化为Map
            return objectMapper.convertValue(entity, Map.class);
        } catch (Exception e) {
            log.error("获取变更前数据失败", e);
            return Collections.emptyMap();
        }
    }
    
    /**
     * 获取变更后的数据
     */
    private Map<String, Object> getAfterData(ProceedingJoinPoint joinPoint, Object result, DataChangeTrack annotation) {
        try {
            if (result == null) {
                return Collections.emptyMap();
            }
            
            // 如果返回值是实体对象,直接序列化
            if (annotation.entityClass().isAssignableFrom(result.getClass())) {
                return objectMapper.convertValue(result, Map.class);
            }
            
            // 如果是更新操作,可能需要重新查询数据库获取最新数据
            if (result instanceof Number) { // 假设返回值是影响的行数
                // 从参数中获取业务ID,查询最新数据
                Object entity = joinPoint.getArgs()[0];
                String businessId = getBusinessId(entity, annotation.businessIdField());
                
                if (businessId != null) {
                    // 查询数据库获取最新数据
                    return queryLatestData(annotation.entityClass(), businessId);
                }
            }
            
            return objectMapper.convertValue(result, Map.class);
        } catch (Exception e) {
            log.error("获取变更后数据失败", e);
            return Collections.emptyMap();
        }
    }
    
    /**
     * 记录数据变更
     */
    private void recordChange(DataChangeTrack annotation, Map<String, Object> beforeData, 
                             Map<String, Object> afterData, Object result) {
        try {
            DataChangeLog logEntry = new DataChangeLog();
            logEntry.setBusinessType(annotation.businessType());
            logEntry.setOperationType(determineOperationType(beforeData, afterData));
            logEntry.setTableName(annotation.entityClass().getSimpleName());
            logEntry.setBeforeValue(objectMapper.writeValueAsString(beforeData));
            logEntry.setAfterValue(objectMapper.writeValueAsString(afterData));
            logEntry.setChangedFields(getChangedFields(beforeData, afterData, annotation.ignoreFields()));
            logEntry.setChangerId(getCurrentUserId());
            logEntry.setChangerName(getCurrentUserName());
            logEntry.setChangeTime(LocalDateTime.now());
            logEntry.setRemark("自动追踪");
            
            // 设置业务ID
            String businessId = getBusinessIdFromResult(result, annotation.businessIdField());
            if (businessId != null) {
                logEntry.setBusinessId(businessId);
            }
            
            logService.save(logEntry);
        } catch (Exception e) {
            log.error("记录数据变更失败", e);
        }
    }
    
    /**
     * 确定操作类型
     */
    private String determineOperationType(Map<String, Object> beforeData, Map<String, Object> afterData) {
        if (beforeData.isEmpty() && !afterData.isEmpty()) {
            return "CREATE";
        } else if (!beforeData.isEmpty() && afterData.isEmpty()) {
            return "DELETE";
        } else {
            return "UPDATE";
        }
    }
    
    /**
     * 获取变更的字段
     */
    private String getChangedFields(Map<String, Object> beforeData, Map<String, Object> afterData, String[] ignoreFields) {
        Set<String> ignored = new HashSet<>(Arrays.asList(ignoreFields));
        List<String> changed = new ArrayList<>();
        
        Set<String> allKeys = new HashSet<>();
        allKeys.addAll(beforeData.keySet());
        allKeys.addAll(afterData.keySet());
        
        for (String key : allKeys) {
            if (ignored.contains(key)) {
                continue;
            }
            
            Object beforeValue = beforeData.get(key);
            Object afterValue = afterData.get(key);
            
            if (!Objects.equals(beforeValue, afterValue)) {
                changed.add(key);
            }
        }
        
        return String.join(",", changed);
    }
    
    /**
     * 获取业务ID
     */
    private String getBusinessId(Object entity, String idField) {
        try {
            Field field = entity.getClass().getDeclaredField(idField);
            field.setAccessible(true);
            Object value = field.get(entity);
            return value != null ? value.toString() : null;
        } catch (Exception e) {
            log.error("获取业务ID失败", e);
            return null;
        }
    }
    
    /**
     * 从结果中获取业务ID
     */
    private String getBusinessIdFromResult(Object result, String idField) {
        if (result != null) {
            try {
                return getBusinessId(result, idField);
            } catch (Exception e) {
                log.error("从结果中获取业务ID失败", e);
            }
        }
        return null;
    }
    
    /**
     * 查询最新数据
     */
    private Map<String, Object> queryLatestData(Class<?> entityClass, String businessId) {
        // 这里需要根据实际情况实现查询逻辑
        // 可以通过反射调用Repository方法
        return Collections.emptyMap();
    }
    
    /**
     * 获取当前用户ID
     */
    private String getCurrentUserId() {
        // 从SecurityContext或ThreadLocal获取当前用户信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
            return ((UserDetails) authentication.getPrincipal()).getUsername();
        }
        return "system";
    }
    
    /**
     * 获取当前用户名
     */
    private String getCurrentUserName() {
        // 实现获取当前用户名的逻辑
        return getCurrentUserId();
    }
}

4. 服务层实现

创建变更日志服务:

@Service
@Transactional
public class DataChangeLogService {
    
    @Autowired
    private DataChangeLogRepository repository;
    
    /**
     * 保存变更记录
     */
    public void save(DataChangeLog log) {
        repository.save(log);
    }
    
    /**
     * 根据业务类型和ID查询变更记录
     */
    public List<DataChangeLog> findByBusinessId(String businessType, String businessId) {
        return repository.findByBusinessTypeAndBusinessIdOrderByChangeTimeDesc(businessType, businessId);
    }
    
    /**
     * 分页查询变更记录
     */
    public Page<DataChangeLog> findLogs(Pageable pageable) {
        return repository.findAll(pageable);
    }
    
    /**
     * 查询指定时间段内的变更记录
     */
    public List<DataChangeLog> findByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
        return repository.findByChangeTimeBetween(startTime, endTime);
    }
}

5. Repository接口

定义数据访问接口:

@Repository
public interface DataChangeLogRepository extends JpaRepository<DataChangeLog, Long> {
    
    List<DataChangeLog> findByBusinessTypeAndBusinessIdOrderByChangeTimeDesc(String businessType, String businessId);
    
    List<DataChangeLog> findByChangeTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
    
    @Query("SELECT d FROM DataChangeLog d WHERE d.businessType = :businessType AND d.operationType = :operationType ORDER BY d.changeTime DESC")
    List<DataChangeLog> findByBusinessTypeAndOperationType(@Param("businessType") String businessType, 
                                                         @Param("operationType") String operationType);
}

6. 使用示例

在业务方法上使用注解:

@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @DataChangeTrack(
        businessType = "USER_UPDATE",
        businessIdField = "id",
        entityClass = User.class,
        trackDetail = true,
        ignoreFields = {"lastModifiedTime", "version"}
    )
    public User updateUser(User user) {
        // 更新用户信息
        return userRepository.save(user);
    }
    
    @DataChangeTrack(
        businessType = "USER_CREATE",
        businessIdField = "id", 
        entityClass = User.class
    )
    public User createUser(User user) {
        // 创建用户
        return userRepository.save(user);
    }
    
    @DataChangeTrack(
        businessType = "USER_DELETE",
        businessIdField = "id",
        entityClass = User.class
    )
    @Transactional
    public void deleteUser(Long userId) {
        // 删除用户
        userRepository.deleteById(userId);
    }
}

@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @DataChangeTrack(
        businessType = "ORDER_STATUS_UPDATE",
        businessIdField = "orderId",
        entityClass = Order.class,
        trackDetail = true,
        ignoreFields = {"updateTime", "version"}
    )
    public Order updateOrderStatus(Order order) {
        // 更新订单状态
        return orderRepository.save(order);
    }
}

7. 控制器接口

提供查询接口:

@RestController
@RequestMapping("/api/data-change")
public class DataChangeController {
    
    @Autowired
    private DataChangeLogService logService;
    
    /**
     * 查询业务对象的变更历史
     */
    @GetMapping("/history")
    public Result<List<DataChangeLog>> getChangeHistory(
            @RequestParam String businessType,
            @RequestParam String businessId) {
        
        List<DataChangeLog> history = logService.findByBusinessId(businessType, businessId);
        return Result.success(history);
    }
    
    /**
     * 分页查询变更记录
     */
    @GetMapping("/logs")
    public Result<Page<DataChangeLog>> getChangeLogs(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "changeTime"));
        Page<DataChangeLog> logs = logService.findLogs(pageable);
        return Result.success(logs);
    }
    
    /**
     * 查询时间范围内的变更记录
     */
    @GetMapping("/logs/time-range")
    public Result<List<DataChangeLog>> getLogsByTimeRange(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
        
        List<DataChangeLog> logs = logService.findByTimeRange(startTime, endTime);
        return Result.success(logs);
    }
}

性能优化策略

1. 异步处理

为了避免影响业务性能,可以将日志记录改为异步:

@Async
public void saveAsync(DataChangeLog log) {
    repository.save(log);
}

2. 批量处理

对于高频变更场景,可以采用批量处理:

@Component
public class BatchLogProcessor {
    
    private final List<DataChangeLog> logBuffer = new ArrayList<>();
    private final Object lock = new Object();
    
    @Scheduled(fixedRate = 5000) // 每5秒批量处理一次
    public void processBatch() {
        synchronized (lock) {
            if (!logBuffer.isEmpty()) {
                List<DataChangeLog> currentBatch = new ArrayList<>(logBuffer);
                logBuffer.clear();
                
                // 批量保存到数据库
                logRepository.saveAll(currentBatch);
            }
        }
    }
    
    public void addToBatch(DataChangeLog log) {
        synchronized (lock) {
            logBuffer.add(log);
        }
    }
}

优势分析

相比传统的手动记录方式,这种方案的优势明显:

  1. 无侵入性:只需添加注解,不影响业务代码
  2. 自动化:自动记录变更,无需手动编写日志代码
  3. 灵活性:可通过注解参数灵活配置
  4. 完整性:记录完整的变更历史
  5. 可追溯:支持按业务类型和ID查询变更历史

注意事项

  1. 性能影响:AOP切面会带来一定的性能开销
  2. 数据安全:注意敏感数据的脱敏处理
  3. 存储容量:变更日志会持续增长,需要定期清理
  4. 事务一致性:确保变更日志与业务操作在同一线程中

总结

通过SpringBoot + AOP + 注解的技术组合,我们可以轻松实现自动化的数据变更追踪。这种方式不仅减少了代码冗余,还提高了系统的可维护性和可追溯性。

在实际项目中,建议根据具体业务需求调整注解参数和切面逻辑,并考虑性能优化策略。


服务端技术精选,专注分享后端开发实战技术,助力你的技术成长!

更多技术文章请访问:www.jiangyi.space


标题:SpringBoot + AOP + 注解 实现自动数据变更追踪实战
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/01/14/1768369395812.html

    0 评论
avatar