SpringBoot + AOP + 注解 实现自动数据变更追踪实战
数据变更追踪的痛点
在我们的日常开发工作中,经常会遇到这样的场景:
- 产品经理问:"这条数据是谁什么时候修改的?"
- 运维人员说:"系统出了问题,需要知道哪些数据发生了变更"
- 审计要求:"需要记录所有的数据变更历史,以便合规检查"
- 业务人员想知道:"这个订单的状态是怎么一步步变过来的"
传统的做法往往是手动在每个业务方法中添加日志记录,不仅代码冗余,还容易遗漏。今天我们就用SpringBoot + AOP + 注解的方式来解决这个问题。
解决方案思路
今天我们要解决的,就是如何用AOP实现自动化的数据变更追踪。
核心思路是:
- 自定义注解:标记需要追踪的方法
- AOP切面:拦截被标记的方法
- 数据对比:比较变更前后的数据差异
- 变更记录:自动记录变更信息
技术选型
- 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);
}
}
}
优势分析
相比传统的手动记录方式,这种方案的优势明显:
- 无侵入性:只需添加注解,不影响业务代码
- 自动化:自动记录变更,无需手动编写日志代码
- 灵活性:可通过注解参数灵活配置
- 完整性:记录完整的变更历史
- 可追溯:支持按业务类型和ID查询变更历史
注意事项
- 性能影响:AOP切面会带来一定的性能开销
- 数据安全:注意敏感数据的脱敏处理
- 存储容量:变更日志会持续增长,需要定期清理
- 事务一致性:确保变更日志与业务操作在同一线程中
总结
通过SpringBoot + AOP + 注解的技术组合,我们可以轻松实现自动化的数据变更追踪。这种方式不仅减少了代码冗余,还提高了系统的可维护性和可追溯性。
在实际项目中,建议根据具体业务需求调整注解参数和切面逻辑,并考虑性能优化策略。
服务端技术精选,专注分享后端开发实战技术,助力你的技术成长!
更多技术文章请访问:www.jiangyi.space
标题:SpringBoot + AOP + 注解 实现自动数据变更追踪实战
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/01/14/1768369395812.html
0 评论