开发者

基于SpringBoot实现自动数据变更追踪需求

目录
  • 背景痛点
    • 传统手工审计的问题
    • 实际遇到的问题
  • 需求分析
    • 核心需求
    • 技术选型考虑
  • 设计思路
    • 整体架构
    • 核心设计
  • 关键代码实现
    • 项目依赖
    • 审计注解
    • 审计切面
    • 业务服务示例
    • 审计日志实体
    • Javers 配置
  • 应用场景示例
    • 场景1:产品信息更新审计
    • 场景2:完整操作历史查询
    • 场景3:删除操作审计
    • 场景4:批量操作审计
  • 总结

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

    背景痛点

    传统手工审计的问题

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

    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字段名,用于从实体中提取ID
        String idField() default "id";
    
        // ID参数名,直接从方法参数中获取ID
        String idParam() default "";
    
        // 操作类型,根据方法名自动推断
        ActionType action() default ActionType.AUTO;
    
        // 操作人参数名xEgXCyDk
        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();
    
            // 提取实体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 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 {
    
        @Bean
        public Javers javers() {
            return JaversBuilder.javers()
                    .withPrettyPrint(true)
        php            .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,\php"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",
        "entityTphpype": "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 + 注解的组合,我们实现了一个零侵入的数据变更审计系统。这个方案的主要优势:

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

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

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

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

    github.com/yuboon/Java-examples/tree/master/springboot-object-version

    到此这篇关于基于SpringBoot实现自动数据变更追踪需求的文章就介绍到这了,更多相关SpringBoot数据变更追踪内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜