news 2026/4/23 14:46:15

<span class=“js_title_inner“>SpringBoot 实现自动数据变更追踪</span>

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
<span class=“js_title_inner“>SpringBoot 实现自动数据变更追踪</span>

👉这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:

  • 《项目实战(视频)》:从书中学,往事上“练”

  • 《互联网高频面试题》:面朝简历学习,春暖花开

  • 《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题

  • 《精进 Java 学习指南》:系统学习,互联网主流技术栈

  • 《必读 Java 源码专栏》:知其然,知其所以然

👉这是一个或许对你有用的开源项目

国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构

RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRMAI大模型、IoT物联网等功能:

  • 多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • 微服务:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本

来源:风象南

  • 背景痛点

  • 需求分析

  • 设计思路

  • 关键代码实现

  • 应用场景示例

  • 总结


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

背景痛点

传统手工审计的问题

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

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

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

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

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

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

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

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

实际遇到的问题
  • 产品价格改错了,查了半天日志才找到是谁改的

  • 配置被误删了,想恢复时发现没有详细的变更记录

  • 审计要求越来越严格,手工记录的日志格式不规范

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

需求分析

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

核心需求

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

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

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

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

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

技术选型考虑

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

  • 专业的对象差异比对算法

  • Spring Boot 集成简单

  • 支持多种存储后端

  • JSON 输出友好

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

设计思路

整体架构

我们采用 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字段名,用于从实体中提取ID String idField() default "id"; // ID参数名,直接从方法参数中获取ID String idParam() default ""; // 操作类型,根据方法名自动推断 ActionType action() default ActionType.AUTO; // 操作人参数名 String actorParam() default ""; // 实体参数位置 int entityIndex() default 0; enum ActionType { CREATE, UPDATE, DELETE, AUTO } }
审计切面
@Slf4j @Aspect @Component @RequiredArgsConstructor publicclass AuditAspect { privatefinal Javers javers; // 内存存储审计日志(生产环境建议使用数据库) privatefinal List<AuditLog> auditTimeline = new CopyOnWriteArrayList<>(); privatefinal Map<String, List<AuditLog>> auditByEntity = new ConcurrentHashMap<>(); privatefinal AtomicLong auditSequence = new AtomicLong(0); // 数据快照存储 privatefinal 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(); // 提取实体ID String 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; } // 辅助方法:提取实体ID private String extractEntityId(Object[] args, String[] paramNames, Audit audit) { // 优先从方法参数中获取ID if (!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; } } } returnnull; } // 其他辅助方法... }
业务服务示例
@Service publicclass ProductService { privatefinal 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) { thrownew 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 { @Bean public 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


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。 谢谢支持哟 (*^__^*)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 18:15:15

【计算机毕业设计案例】基于ssm的高校环保公益网站的设计与开发基于SSM的社会公益平台(程序+文档+讲解+定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/4/23 14:10:04

【课程设计/毕业设计】基于ssm的体育器材管理系统设计与实现高校体育器材管理系统的设计与实现【附源码、数据库、万字文档】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/4/23 11:20:40

java毕设选题推荐:基于java的短剧推荐系统设计与实现基于Java+SSM的短剧推荐系统设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/4/23 11:20:27

看完就会:8个一键生成论文工具测评,专科生毕业论文轻松搞定!

对于专科生而言&#xff0c;毕业论文写作不仅是学业的终点&#xff0c;更是能力的考验。然而&#xff0c;面对选题困难、资料查找繁琐、格式规范复杂等问题&#xff0c;许多同学感到无从下手。为了帮助大家更高效地完成论文&#xff0c;我们基于2026年的实测数据与用户反馈&…

作者头像 李华