核心内容摘要
Qwen-Image-Edit-2511上手难度实测:技术小白也能成功
在企业级应用中关键配置、业务数据变更的审计追踪是一个常见需求。
无论是金融系统、电商平台还是配置管理都需要回答几个基本问题谁改了数据、什么时候改的、改了什么。
背景痛点传统手工审计的问题最直接的实现方式是在每个业务方法中手动记录审计日志public void updatePrice(Long productId, BigDecimal newPrice) { Product old productRepository.findById(productId).get(); productRepository.updatePrice(productId, newPrice); // 手动记录变更 auditService.save(价格从 old.getPrice() 改为 newPrice); }这种做法在项目初期还能应付但随着业务复杂度增加会暴露出几个明显问题•代码重复每个需要审计的方法都要写类似逻辑•维护困难业务字段变更时审计逻辑需要同步修改•格式不统一不同开发者写的审计格式可能不一致•查询不便字符串拼接的日志难以进行结构化查询•业务代码污染审计逻辑与业务逻辑耦合在一起实际遇到的问题• 产品价格改错了查了半天日志才找到是谁改的• 配置被误删了想恢复时发现没有详细的变更记录• 审计要求越来越严格手工记录的日志格式不规范需求分析基于实际需求审计功能应具备以下特性核心需求•零侵入性业务代码不需要关心审计逻辑•自动化通过配置或注解就能启用审计功能•精确记录字段级别的变更追踪•结构化存储便于查询和分析的格式•完整信息包含操作人、时间、操作类型等元数据技术选型考虑本方案选择使用 Javers 作为核心组件主要考虑• 专业的对象差异比对算法• Spring Boot 集成简单• 支持多种存储后端• JSON 输出友好设计思路整体架构我们采用 AOP 注解的设计模式┌─────────────────┐ │ Controller │ └─────────┬───────┘ │ AOP 拦截 ┌─────────▼───────┐ │ Service │ ← 业务逻辑保持不变 └─────────┬───────┘ │ ┌─────────▼───────┐ │ AuditAspect │ ← 统一处理审计逻辑 └─────────┬───────┘ │ ┌─────────▼───────┐ │ Javers Core │ ← 对象差异比对 └─────────┬───────┘ │ ┌─────────▼───────┐ │ Audit Storage │ ← 结构化存储 └─────────────────┘核心设计•注解驱动通过 Audit 注解标记需要审计的方法•切面拦截AOP 自动拦截带注解的方法•差异比对使用 Javers 比较对象变更•统一存储审计日志统一存储和查询关键代码实现项目依赖dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency dependency groupIdorg.javers/groupId artifactIdjavers-core/artifactId version
7.
1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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 public class AuditAspect { private final Javers javers; // 内存存储审计日志生产环境建议使用数据库 private final ListAuditLog auditTimeline new CopyOnWriteArrayList(); private final MapString, ListAuditLog auditByEntity new ConcurrentHashMap(); private final AtomicLong auditSequence new AtomicLong(
; // 数据快照存储 private final MapString, 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; } } } return null; } // 其他辅助方法... }业务服务示例Service public class ProductService { private final MapString, 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 { 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:
9
99, description: 最新款手机 }审计日志结构{ id: 1, entityType: Product, entityId: prod-001, action: UPDATE, actor: 张三, occurredAt:
T10:30:00Z, diffJson: {\changes\:[{\field\:\price\,\oldValue\:
1
00,\newValue\:
9
99}]} }diffJson 的具体内容{ changes: [ { changeType: ValueChange, globalId: { valueObject: com.example.objectversion.dto.ProductRequest }, property: price, propertyChangeType: PROPERTY_VALUE_CHANGED, left:
1
00, right:
9
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:
T08:00:00Z, diffJson: {\changes\:[{\field\:\name\,\oldValue\:null,\newValue\:\iPhone 15\},{\field\:\price\,\oldValue\:null,\newValue\:
1
00}]} }, { id: 2, entityType: Product, entityId: prod-001, action: UPDATE, actor: 张三, occurredAt:
T10:30:00Z, diffJson: {\changes\:[{\field\:\price\,\oldValue\:
1
00,\newValue\:
9
99}]} } ]场景3删除操作审计删除请求DELETE /api/products/prod-001 X-User: 李四审计日志{ id: 3, entityType: Product, entityId: prod-001, action: DELETE, actor: 李四, occurredAt:
T15:45:00Z, diffJson: {\changes\:[]} }场景4批量操作审计创建多个产品// 执行多次创建操作 productService.create(prod-002, new ProductRequest(手机壳,
2
99, 透明保护壳), 王
; productService.create(prod-003, new ProductRequest(充电器,
5
99, 快充充电器), 王
;审计日志[ { id: 4, entityType: Product, entityId: prod-002, action: CREATE, actor: 王五, occurredAt:
T16:00:00Z, diffJson: {\changes\:[{\field\:\name\,\oldValue\:null,\newValue\:\手机壳\},{\field\:\price\,\oldValue\:null,\newValue\:
2
99}]} }, { id: 5, entityType: Product, entityId: prod-003, action: CREATE, actor: 王五, occurredAt:
T16:01:00Z, diffJson: {\changes\:[{\field\:\name\,\oldValue\:null,\newValue\:\充电器\},{\field\:\price\,\oldValue\:null,\newValue\:
5
99}]} } ]
总结通过 Javers AOP 注解的组合我们实现了一个零侵入的数据变更审计系统。
这个方案的主要优势•开发效率提升无需在每个业务方法中编写审计逻辑•维护成本降低审计逻辑集中在切面中便于统一管理•数据质量改善结构化的审计日志便于查询和分析技术方案没有银弹需要根据具体业务场景进行调整。
如果您的项目也有数据审计需求这个方案可以作为参考。