最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于SpringBoot實(shí)現(xiàn)自動(dòng)數(shù)據(jù)變更追蹤需求

 更新時(shí)間:2025年10月12日 08:44:08   作者:風(fēng)象南  
在企業(yè)級(jí)應(yīng)用中,關(guān)鍵配置,業(yè)務(wù)數(shù)據(jù)變更的審計(jì)追蹤是一個(gè)常見(jiàn)需求,下面小編就要和大家簡(jiǎn)單介紹一下如何使用SpringBoot實(shí)現(xiàn)自動(dòng)數(shù)據(jù)變更的需求吧

在企業(yè)級(jí)應(yīng)用中,關(guān)鍵配置、業(yè)務(wù)數(shù)據(jù)變更的審計(jì)追蹤是一個(gè)常見(jiàn)需求。無(wú)論是金融系統(tǒng)、電商平臺(tái)還是配置管理,都需要回答幾個(gè)基本問(wèn)題:誰(shuí)改了數(shù)據(jù)、什么時(shí)候改的、改了什么。

背景痛點(diǎn)

傳統(tǒng)手工審計(jì)的問(wèn)題

最直接的實(shí)現(xiàn)方式是在每個(gè)業(yè)務(wù)方法中手動(dòng)記錄審計(jì)日志:

public void updatePrice(Long productId, BigDecimal newPrice) {
    Product old = productRepository.findById(productId).get();
    productRepository.updatePrice(productId, newPrice);

    // 手動(dòng)記錄變更
    auditService.save("價(jià)格從 " + old.getPrice() + " 改為 " + newPrice);
}

這種做法在項(xiàng)目初期還能應(yīng)付,但隨著業(yè)務(wù)復(fù)雜度增加,會(huì)暴露出幾個(gè)明顯問(wèn)題:

代碼重復(fù):每個(gè)需要審計(jì)的方法都要寫(xiě)類(lèi)似邏輯

維護(hù)困難:業(yè)務(wù)字段變更時(shí),審計(jì)邏輯需要同步修改

格式不統(tǒng)一:不同開(kāi)發(fā)者寫(xiě)的審計(jì)格式可能不一致

查詢(xún)不便:字符串拼接的日志難以進(jìn)行結(jié)構(gòu)化查詢(xún)

業(yè)務(wù)代碼污染:審計(jì)邏輯與業(yè)務(wù)邏輯耦合在一起

實(shí)際遇到的問(wèn)題

  • 產(chǎn)品價(jià)格改錯(cuò)了,查了半天日志才找到是誰(shuí)改的
  • 配置被誤刪了,想恢復(fù)時(shí)發(fā)現(xiàn)沒(méi)有詳細(xì)的變更記錄
  • 審計(jì)要求越來(lái)越嚴(yán)格,手工記錄的日志格式不規(guī)范

需求分析

基于實(shí)際需求,審計(jì)功能應(yīng)具備以下特性:

核心需求

1. 零侵入性:業(yè)務(wù)代碼不需要關(guān)心審計(jì)邏輯

2. 自動(dòng)化:通過(guò)配置或注解就能啟用審計(jì)功能

3. 精確記錄:字段級(jí)別的變更追蹤

4. 結(jié)構(gòu)化存儲(chǔ):便于查詢(xún)和分析的格式

5. 完整信息:包含操作人、時(shí)間、操作類(lèi)型等元數(shù)據(jù)

技術(shù)選型考慮

本方案選擇使用 Javers 作為核心組件,主要考慮:

  • 專(zhuān)業(yè)的對(duì)象差異比對(duì)算法
  • Spring Boot 集成簡(jiǎn)單
  • 支持多種存儲(chǔ)后端
  • JSON 輸出友好

設(shè)計(jì)思路

整體架構(gòu)

我們采用 AOP + 注解的設(shè)計(jì)模式:

┌─────────────────┐
│   Controller    │
└─────────┬───────┘
          │ AOP 攔截
┌─────────▼───────┐
│     Service     │ ← 業(yè)務(wù)邏輯保持不變
└─────────┬───────┘
          │
┌─────────▼───────┐
│   AuditAspect   │ ← 統(tǒng)一處理審計(jì)邏輯
└─────────┬───────┘
          │
┌─────────▼───────┐
│   Javers Core   │ ← 對(duì)象差異比對(duì)
└─────────┬───────┘
          │
┌─────────▼───────┐
│  Audit Storage  │ ← 結(jié)構(gòu)化存儲(chǔ)
└─────────────────┘

核心設(shè)計(jì)

1. 注解驅(qū)動(dòng):通過(guò) @Audit 注解標(biāo)記需要審計(jì)的方法

2. 切面攔截:AOP 自動(dòng)攔截帶注解的方法

3. 差異比對(duì):使用 Javers 比較對(duì)象變更

4. 統(tǒng)一存儲(chǔ):審計(jì)日志統(tǒng)一存儲(chǔ)和查詢(xún)

關(guān)鍵代碼實(shí)現(xiàn)

項(xiàng)目依賴(lài)

<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>

審計(jì)注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {
    // ID字段名,用于從實(shí)體中提取ID
    String idField() default "id";

    // ID參數(shù)名,直接從方法參數(shù)中獲取ID
    String idParam() default "";

    // 操作類(lèi)型,根據(jù)方法名自動(dòng)推斷
    ActionType action() default ActionType.AUTO;

    // 操作人參數(shù)名
    String actorParam() default "";

    // 實(shí)體參數(shù)位置
    int entityIndex() default 0;

    enum ActionType {
        CREATE, UPDATE, DELETE, AUTO
    }
}

審計(jì)切面

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AuditAspect {

    private final Javers javers;

    // 內(nèi)存存儲(chǔ)審計(jì)日志(生產(chǎn)環(huán)境建議使用數(shù)據(jù)庫(kù))
    private final List<AuditLog> auditTimeline = new CopyOnWriteArrayList<>();
    private final Map<String, List<AuditLog>> auditByEntity = new ConcurrentHashMap<>();
    private final AtomicLong auditSequence = new AtomicLong(0);

    // 數(shù)據(jù)快照存儲(chǔ)
    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();

        // 提取實(shí)體ID
        String entityId = extractEntityId(args, paramNames, auditAnnotation);
        if (entityId == null) {
            log.warn("無(wú)法提取實(shí)體ID,跳過(guò)審計(jì): {}", method.getName());
            return joinPoint.proceed();
        }

        // 提取實(shí)體對(duì)象
        Object entity = null;
        if (auditAnnotation.entityIndex() >= 0 && auditAnnotation.entityIndex() < args.length) {
            entity = args[auditAnnotation.entityIndex()];
        }

        // 提取操作人
        String actor = extractActor(args, paramNames, auditAnnotation);

        // 確定操作類(lèi)型
        Audit.ActionType actionType = determineActionType(auditAnnotation, method.getName());

        // 執(zhí)行前快照
        Object beforeSnapshot = dataStore.get(buildKey(entityId));

        // 執(zhí)行原方法
        Object result = joinPoint.proceed();

        // 執(zhí)行后快照
        Object afterSnapshot = determineAfterSnapshot(entity, actionType);

        // 比較差異并記錄審計(jì)日志
        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)
            );
        }

        // 更新數(shù)據(jù)存儲(chǔ)
        if (actionType != Audit.ActionType.DELETE) {
            dataStore.put(buildKey(entityId), afterSnapshot);
        } else {
            dataStore.remove(buildKey(entityId));
        }

        return result;
    }

    // 輔助方法:提取實(shí)體ID
    private String extractEntityId(Object[] args, String[] paramNames, Audit audit) {
        // 優(yōu)先從方法參數(shù)中獲取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;
    }

    // 其他輔助方法...
}

業(yè)務(wù)服務(wù)示例

@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("產(chǎn)品不存在: " + 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);
    }
}

審計(jì)日志實(shí)體

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();
    }
}

應(yīng)用場(chǎng)景示例

場(chǎng)景1:產(chǎn)品信息更新審計(jì)

操作請(qǐng)求

PUT /api/products/prod-001
Content-Type: application/json
X-User: 張三

{
  "name": "iPhone 15",
  "price": 99.99,
  "description": "最新款手機(jī)"
}

審計(jì)日志結(jié)構(gòu)

{
  "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 的具體內(nèi)容

{
  "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": "最新款手機(jī)"
    }
  ]
}

場(chǎng)景2:完整操作歷史查詢(xún)

GET /api/products/prod-001/audits

響應(yīng)結(jié)果

[
  {
    "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}]}"
  }
]

場(chǎng)景3:刪除操作審計(jì)

刪除請(qǐng)求

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

審計(jì)日志

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

場(chǎng)景4:批量操作審計(jì)

創(chuàng)建多個(gè)產(chǎn)品

// 執(zhí)行多次創(chuàng)建操作
productService.create("prod-002", new ProductRequest("手機(jī)殼", 29.99, "透明保護(hù)殼"), "王五");
productService.create("prod-003", new ProductRequest("充電器", 59.99, "快充充電器"), "王五");

審計(jì)日志

[
  {
    "id": "4",
    "entityType": "Product",
    "entityId": "prod-002",
    "action": "CREATE",
    "actor": "王五",
    "occurredAt": "2025-10-13T16:00:00Z",
    "diffJson": "{\"changes\":[{\"field\":\"name\",\"oldValue\":null,\"newValue\":\"手機(jī)殼\"},{\"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}]}"
  }
]

總結(jié)

通過(guò) Javers + AOP + 注解的組合,我們實(shí)現(xiàn)了一個(gè)零侵入的數(shù)據(jù)變更審計(jì)系統(tǒng)。這個(gè)方案的主要優(yōu)勢(shì):

開(kāi)發(fā)效率提升:無(wú)需在每個(gè)業(yè)務(wù)方法中編寫(xiě)審計(jì)邏輯

維護(hù)成本降低:審計(jì)邏輯集中在切面中,便于統(tǒng)一管理

數(shù)據(jù)質(zhì)量改善:結(jié)構(gòu)化的審計(jì)日志便于查詢(xún)和分析

技術(shù)方案沒(méi)有銀彈,需要根據(jù)具體業(yè)務(wù)場(chǎng)景進(jìn)行調(diào)整。如果您的項(xiàng)目也有數(shù)據(jù)審計(jì)需求,這個(gè)方案可以作為參考。

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

到此這篇關(guān)于基于SpringBoot實(shí)現(xiàn)自動(dòng)數(shù)據(jù)變更追蹤需求的文章就介紹到這了,更多相關(guān)SpringBoot數(shù)據(jù)變更追蹤內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java模板引擎Thymeleaf基本語(yǔ)法詳解

    Java模板引擎Thymeleaf基本語(yǔ)法詳解

    當(dāng)開(kāi)發(fā)Web應(yīng)用程序時(shí),我們通常需要使用模板引擎來(lái)構(gòu)建和呈現(xiàn)動(dòng)態(tài)內(nèi)容,Thymeleaf是一個(gè)功能強(qiáng)大的Java模板引擎,它提供了豐富的表達(dá)式和標(biāo)簽,使得數(shù)據(jù)綁定、條件判斷、循環(huán)迭代等操作變得輕松而靈活,本文就簡(jiǎn)單的給大家介紹一下Thymeleaf基本語(yǔ)法
    2023-08-08
  • springBoot靜態(tài)資源加載不到,并且配置了也不生效問(wèn)題及解決

    springBoot靜態(tài)資源加載不到,并且配置了也不生效問(wèn)題及解決

    這篇文章總結(jié)了一個(gè)在Spring Boot 2.6.x版本中,由于路徑匹配策略改變導(dǎo)致靜態(tài)資源無(wú)法加載的問(wèn)題,并提供了解決方案:通過(guò)配置類(lèi)或在配置文件中設(shè)置路徑匹配策略為AntPathMatcher,或者直接降級(jí)Spring Boot版本
    2025-02-02
  • Java(springboot) 讀取txt文本內(nèi)容代碼實(shí)例

    Java(springboot) 讀取txt文本內(nèi)容代碼實(shí)例

    這篇文章主要介紹了Java(springboot) 讀取txt文本內(nèi)容代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-02-02
  • java實(shí)現(xiàn)隨機(jī)驗(yàn)證碼圖片生成

    java實(shí)現(xiàn)隨機(jī)驗(yàn)證碼圖片生成

    這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)隨機(jī)驗(yàn)證碼圖片生成,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • 聊聊Java 中的線(xiàn)程中斷

    聊聊Java 中的線(xiàn)程中斷

    這篇文章主要介紹了Java 中的線(xiàn)程中斷的相關(guān)資料,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下
    2020-11-11
  • 詳解Java中Object?類(lèi)的使用

    詳解Java中Object?類(lèi)的使用

    Java的Object?類(lèi)是所有類(lèi)的父類(lèi),也就是說(shuō)?Java?的所有類(lèi)都繼承了?Object,本文主要來(lái)和大家講講Object?類(lèi)的使用,感興趣的可以了解一下
    2023-05-05
  • 基于ReentrantLock的實(shí)現(xiàn)原理講解

    基于ReentrantLock的實(shí)現(xiàn)原理講解

    這篇文章主要介紹了ReentrantLock的實(shí)現(xiàn)原理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • 基于MyBatis的關(guān)聯(lián)查詢(xún)優(yōu)化與應(yīng)用實(shí)踐

    基于MyBatis的關(guān)聯(lián)查詢(xún)優(yōu)化與應(yīng)用實(shí)踐

    在實(shí)際項(xiàng)目開(kāi)發(fā)中,關(guān)聯(lián)查詢(xún)是一種常見(jiàn)的需求,尤其是當(dāng)涉及到多個(gè)表之間的數(shù)據(jù)統(tǒng)計(jì)、關(guān)聯(lián)查詢(xún)以及嵌套對(duì)象的構(gòu)建時(shí),如何確保數(shù)據(jù)的準(zhǔn)確性和實(shí)時(shí)性,是開(kāi)發(fā)者必須面對(duì)的挑戰(zhàn),本文將介紹基于MyBatis的關(guān)聯(lián)查詢(xún)優(yōu)化與應(yīng)用實(shí)踐,需要的朋友可以參考下
    2024-12-12
  • SpringBoot項(xiàng)目的五種創(chuàng)建方式

    SpringBoot項(xiàng)目的五種創(chuàng)建方式

    這篇文章主要介紹了SpringBoot項(xiàng)目的五種創(chuàng)建方式,文中通過(guò)圖文結(jié)合的方式講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下
    2024-12-12
  • IntelliJ IDEA使用git初始化倉(cāng)庫(kù)的使用方法

    IntelliJ IDEA使用git初始化倉(cāng)庫(kù)的使用方法

    這篇文章主要介紹了IntelliJ IDEA使用git初始化倉(cāng)庫(kù)的使用方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-04-04

最新評(píng)論

加查县| 杨浦区| 拉萨市| 无棣县| 仪陇县| 松滋市| 嵊泗县| 林口县| 太仆寺旗| 如皋市| 崇左市| 灵璧县| 绥滨县| 顺义区| 油尖旺区| 东丽区| 修文县| 江山市| 娱乐| 五指山市| 宁化县| 丰原市| 浏阳市| 澜沧| 高安市| 巴林左旗| 新津县| 昌江| 柘荣县| 九龙城区| 杨浦区| 保康县| 青川县| 武山县| 平原县| 城口县| 通州区| 娄烦县| 通州市| 温州市| 牟定县|