当前位置: 首页 > news >正文

SpringBoot 实现自动数据变更追踪

在企业级应用中,关键配置、业务数据变更的审计追踪是一个常见需求。无论是金融系统、电商平台还是配置管理,都需要回答几个基本问题:谁改了数据、什么时候改的、改了什么。

背景痛点

传统手工审计的问题

最直接的实现方式是在每个业务方法中手动记录审计日志:

public void updatePrice(Long productId, BigDecimal newPrice) {Product old = productRepository.findById(productId).get();productRepository.updatePrice(productId, newPrice);// 手动记录变更auditService.save("价格从 " + old.getPrice() + " 改为 " + newPrice);
}

这种做法在项目初期还能应付,但随着业务复杂度增加,会暴露出几个明显问题:

代码重复:每个需要审计的方法都要写类似逻辑

维护困难:业务字段变更时,审计逻辑需要同步修改

格式不统一:不同开发者写的审计格式可能不一致

查询不便:字符串拼接的日志难以进行结构化查询

业务代码污染:审计逻辑与业务逻辑耦合在一起

实际遇到的问题

  • 产品价格改错了,查了半天日志才找到是谁改的
  • 配置被误删了,想恢复时发现没有详细的变更记录
  • 审计要求越来越严格,手工记录的日志格式不规范

需求分析

基于实际需求,审计功能应具备以下特性:

核心需求

1. 零侵入性:业务代码不需要关心审计逻辑

2. 自动化:通过配置或注解就能启用审计功能

3. 精确记录:字段级别的变更追踪

4. 结构化存储:便于查询和分析的格式

5. 完整信息:包含操作人、时间、操作类型等元数据

技术选型考虑

本方案选择使用 Javers 作为核心组件,主要考虑:

  • 专业的对象差异比对算法
  • Spring Boot 集成简单
  • 支持多种存储后端
  • JSON 输出友好

设计思路

整体架构

我们采用 AOP + 注解的设计模式:

┌─────────────────┐
│   Controller    │
└─────────┬───────┘│ AOP 拦截
┌─────────▼───────┐
│     Service     │ ← 业务逻辑保持不变
└─────────┬───────┘│
┌─────────▼───────┐
│   AuditAspect   │ ← 统一处理审计逻辑
└─────────┬───────┘│
┌─────────▼───────┐
│   Javers Core   │ ← 对象差异比对
└─────────┬───────┘│
┌─────────▼───────┐
│  Audit Storage  │ ← 结构化存储
└─────────────────┘

核心设计

1. 注解驱动:通过 @Audit 注解标记需要审计的方法

2. 切面拦截:AOP 自动拦截带注解的方法

3. 差异比对:使用 Javers 比较对象变更

4. 统一存储:审计日志统一存储和查询

关键代码实现

项目依赖

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.javers</groupId><artifactId>javers-core</artifactId><version>7.3.1</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>

审计注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {// ID字段名,用于从实体中提取IDString idField() default "id";// ID参数名,直接从方法参数中获取IDString idParam() default "";// 操作类型,根据方法名自动推断ActionType action() default ActionType.AUTO;// 操作人参数名String actorParam() default "";// 实体参数位置int entityIndex() default 0;enum ActionType {CREATE, UPDATE, DELETE, AUTO}
}

审计切面

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AuditAspect {private final Javers javers;// 内存存储审计日志(生产环境建议使用数据库)private final List<AuditLog> auditTimeline = new CopyOnWriteArrayList<>();private final Map<String, List<AuditLog>> auditByEntity = new ConcurrentHashMap<>();private final AtomicLong auditSequence = new AtomicLong(0);// 数据快照存储private final Map<String, Object> dataStore = new ConcurrentHashMap<>();@Around("@annotation(auditAnnotation)")public Object auditMethod(ProceedingJoinPoint joinPoint, Audit auditAnnotation) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();String[] paramNames = signature.getParameterNames();Object[] args = joinPoint.getArgs();// 提取实体IDString entityId = extractEntityId(args, paramNames, auditAnnotation);if (entityId == null) {log.warn("无法提取实体ID,跳过审计: {}", method.getName());return joinPoint.proceed();}// 提取实体对象Object entity = null;if (auditAnnotation.entityIndex() >= 0 && auditAnnotation.entityIndex() < args.length) {entity = args[auditAnnotation.entityIndex()];}// 提取操作人String actor = extractActor(args, paramNames, auditAnnotation);// 确定操作类型Audit.ActionType actionType = determineActionType(auditAnnotation, method.getName());// 执行前快照Object beforeSnapshot = dataStore.get(buildKey(entityId));// 执行原方法Object result = joinPoint.proceed();// 执行后快照Object afterSnapshot = determineAfterSnapshot(entity, actionType);// 比较差异并记录审计日志Diff diff = javers.compare(beforeSnapshot, afterSnapshot);if (diff.hasChanges() || beforeSnapshot == null || actionType == Audit.ActionType.DELETE) {recordAudit(entity != null ? entity.getClass().getSimpleName() : "Unknown",entityId,actionType.name(),actor,javers.getJsonConverter().toJson(diff));}// 更新数据存储if (actionType != Audit.ActionType.DELETE) {dataStore.put(buildKey(entityId), afterSnapshot);} else {dataStore.remove(buildKey(entityId));}return result;}// 辅助方法:提取实体IDprivate String extractEntityId(Object[] args, String[] paramNames, Audit audit) {// 优先从方法参数中获取IDif (!audit.idParam().isEmpty() && paramNames != null) {for (int i = 0; i < paramNames.length; i++) {if (audit.idParam().equals(paramNames[i])) {Object idValue = args[i];return idValue != null ? idValue.toString() : null;}}}return null;}// 其他辅助方法...
}

业务服务示例

@Service
public class ProductService {private final Map<String, Product> products = new ConcurrentHashMap<>();@Audit(action = Audit.ActionType.CREATE,idParam = "id",actorParam = "actor",entityIndex = 1)public Product create(String id, ProductRequest request, String actor) {Product newProduct = new Product(id, request.name(), request.price(), request.description());return products.put(id, newProduct);}@Audit(action = Audit.ActionType.UPDATE,idParam = "id",actorParam = "actor",entityIndex = 1)public Product update(String id, ProductRequest request, String actor) {Product existingProduct = products.get(id);if (existingProduct == null) {throw new IllegalArgumentException("产品不存在: " + id);}Product updatedProduct = new Product(id, request.name(), request.price(), request.description());return products.put(id, updatedProduct);}@Audit(action = Audit.ActionType.DELETE,idParam = "id",actorParam = "actor")public boolean delete(String id, String actor) {return products.remove(id) != null;}@Audit(idParam = "id",actorParam = "actor",entityIndex = 1)public Product upsert(String id, ProductRequest request, String actor) {Product newProduct = new Product(id, request.name(), request.price(), request.description());return products.put(id, newProduct);}
}

审计日志实体

public record AuditLog(String id,String entityType,String entityId,String action,String actor,Instant occurredAt,String diffJson
) {}

Javers 配置

@Configuration
public class JaversConfig {@Beanpublic Javers javers() {return JaversBuilder.javers().withPrettyPrint(true).build();}
}

应用场景示例

场景1:产品信息更新审计

操作请求

PUT /api/products/prod-001
Content-Type: application/json
X-User: 张三{"name": "iPhone 15","price": 99.99,"description": "最新款手机"
}

审计日志结构

{"id": "1","entityType": "Product","entityId": "prod-001","action": "UPDATE","actor": "张三","occurredAt": "2025-10-12T10:30:00Z","diffJson": "{\"changes\":[{\"field\":\"price\",\"oldValue\":100.00,\"newValue\":99.99}]}"
}

diffJson 的具体内容

{"changes": [{"changeType": "ValueChange","globalId": {"valueObject": "com.example.objectversion.dto.ProductRequest"},"property": "price","propertyChangeType": "PROPERTY_VALUE_CHANGED","left": 100.00,"right": 99.99},{"changeType": "ValueChange","globalId": {"valueObject": "com.example.objectversion.dto.ProductRequest"},"property": "description","propertyChangeType": "PROPERTY_VALUE_CHANGED","left": null,"right": "最新款手机"}]
}

场景2:完整操作历史查询

GET /api/products/prod-001/audits

响应结果

[{"id": "1","entityType": "Product","entityId": "prod-001","action": "CREATE","actor": "system","occurredAt": "2025-10-10T08:00:00Z","diffJson": "{\"changes\":[{\"field\":\"name\",\"oldValue\":null,\"newValue\":\"iPhone 15\"},{\"field\":\"price\",\"oldValue\":null,\"newValue\":100.00}]}"},{"id": "2","entityType": "Product","entityId": "prod-001","action": "UPDATE","actor": "张三","occurredAt": "2025-10-12T10:30:00Z","diffJson": "{\"changes\":[{\"field\":\"price\",\"oldValue\":100.00,\"newValue\":99.99}]}"}
]

场景3:删除操作审计

删除请求

DELETE /api/products/prod-001
X-User: 李四

审计日志

{"id": "3","entityType": "Product","entityId": "prod-001","action": "DELETE","actor": "李四","occurredAt": "2025-10-13T15:45:00Z","diffJson": "{\"changes\":[]}"
}

场景4:批量操作审计

创建多个产品

// 执行多次创建操作
productService.create("prod-002", new ProductRequest("手机壳", 29.99, "透明保护壳"), "王五");
productService.create("prod-003", new ProductRequest("充电器", 59.99, "快充充电器"), "王五");

审计日志

[{"id": "4","entityType": "Product","entityId": "prod-002","action": "CREATE","actor": "王五","occurredAt": "2025-10-13T16:00:00Z","diffJson": "{\"changes\":[{\"field\":\"name\",\"oldValue\":null,\"newValue\":\"手机壳\"},{\"field\":\"price\",\"oldValue\":null,\"newValue\":29.99}]}"},{"id": "5","entityType": "Product","entityId": "prod-003","action": "CREATE","actor": "王五","occurredAt": "2025-10-13T16:01:00Z","diffJson": "{\"changes\":[{\"field\":\"name\",\"oldValue\":null,\"newValue\":\"充电器\"},{\"field\":\"price\",\"oldValue\":null,\"newValue\":59.99}]}"}
]

总结

通过 Javers + AOP + 注解的组合,我们实现了一个零侵入的数据变更审计系统。这个方案的主要优势:

开发效率提升:无需在每个业务方法中编写审计逻辑

维护成本降低:审计逻辑集中在切面中,便于统一管理

数据质量改善:结构化的审计日志便于查询和分析

技术方案没有银弹,需要根据具体业务场景进行调整。如果您的项目也有数据审计需求,这个方案可以作为参考。

https://github.com/yuboon/java-examples/tree/master/springboot-object-version

http://www.dtcms.com/a/473360.html

相关文章:

  • C语言⽂件操作讲解(3)
  • 对网站做数据分析北京市建设工程信息
  • 1.6虚拟机
  • XCP服务
  • Excel - Excel 列出一列中所有不重复数据
  • 如何获取用户右击的Excel单元格位置
  • 昆明企业网站建设公司虹口建设机械网站制作
  • 宁波p2p网站建设黑龙江省建设安全网站
  • Spring Boot 3零基础教程,自动配置机制,笔记07
  • Spring通关笔记:从“Hello Bean”到循环依赖的奇幻漂流
  • 【Spring Security】Spring Security 密码编辑器
  • MCU ADC外设工作原理介绍
  • k8s的ymal文件
  • 杭州公司建设网站网站建设标签
  • 博客系统小笔记
  • 后端开发和软件开发有什么区别
  • 分布式专题——41 RocketMQ集群高级特性
  • 自然语言处理分享系列-词语和短语的分布式表示及其组合性(一)
  • 从0到1实现鸿蒙智能设备状态监控:轻量级架构、分布式同步与MQTT实战全解析
  • RWKV架构讲解
  • Docker 镜像维护指南:从配置优化到 MySQL 实战运行
  • 电视盒子助手开心电视助手 v8.0 删除电视内置软件 电视远程控制ADB去除电视广告
  • 【完整源码+数据集+部署教程】 航拍杂草检测与分类系统源码和数据集:改进yolo11-RVB-EMA
  • My SQL--创建数据库、表
  • mysql高可用架构之MHA部署(三)——故障转移后邮件告警配置(保姆级)
  • 做酒的网站有哪些jsp获取网站域名
  • OpenCV(八):NumPy
  • 小微宝安网站建设有哪些做分析图用的网站
  • RabbitMQ 核心概念解析
  • 开发实战 - ego商城 - 2 nodejs搭建后端环境