👉这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
《项目实战(视频)》:从书中学,往事上“练”
《互联网高频面试题》:面朝简历学习,春暖花开
《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题
《精进 Java 学习指南》:系统学习,互联网主流技术栈
《必读 Java 源码专栏》:知其然,知其所以然
👉这是一个或许对你有用的开源项目
国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构
RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRM、AI大模型、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
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。 谢谢支持哟 (*^__^*)