1. 概述

本文将介绍如何在一个简单的 Spring Boot 应用中集成并使用 JaVers,实现对实体变更的追踪。

在实际开发中,数据库通常只保存实体的最新状态。当需要排查某个字段为何被修改、谁在何时操作过数据时,往往只能靠翻日志,效率极低,尤其在生产环境多用户并发操作下,问题更难定位。

而 JaVers 正是为解决这类问题而生的审计框架,它能自动记录实体的每一次变更,帮你轻松实现数据溯源、操作日志、安全审计等功能,不再“盲人摸象”。

2. 什么是 JaVers

JaVers 是一个轻量级的 Java 对象审计框架,专注于追踪实体对象的变更历史(Change Tracking)。它的核心能力包括:

  • ✅ 记录每次变更的完整快照(Snapshot)
  • ✅ 支持按实体、按字段、按时间查询变更
  • ✅ 自动生成变更差异(Diff)
  • ✅ 支持多种存储(SQL、MongoDB)
  • ✅ 与 Spring Boot、JPA 无缝集成

除了调试和审计,JaVers 还可用于:

  • 数据变更分析
  • 实现简易版事件溯源(Event Sourcing)
  • 安全校验与合规性报告
  • 用户操作日志展示

简单来说,它就是给你的数据装了一个“黑匣子”。

3. 项目配置

要使用 JaVers,主要涉及三部分配置:依赖引入、存储配置、属性设置和领域模型定义。不过得益于 Spring Boot 的自动装配,大部分场景下几乎“开箱即用”。

3.1. 依赖引入

首先引入 JaVers 的 Spring Boot Starter。根据你使用的持久化技术,选择对应版本:

  • SQL 存储:javers-spring-boot-starter-sql
  • MongoDB 存储:javers-spring-boot-starter-mongo

本文使用 H2 + SQL 方案:

<dependency>
    <groupId>org.javers</groupId>
    <artifactId>javers-spring-boot-starter-sql</artifactId>
    <version>6.6.5</version>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

✅ 注意:只要引入 starter,JaVers 会自动检测数据源并完成初始化,无需手动配置数据源。

3.2. 存储仓库配置

JaVers 使用 Repository 模式来持久化提交记录(Commit)和实体快照,所有数据以 JSON 格式存储。

默认情况下:

  • 使用内存仓库(javers-in-memory-repository),重启后数据丢失
  • 若项目中已集成 Spring Data JPA,JaVers 会自动复用现有数据库配置,将审计数据写入同一数据库

对于 SQL 方案,JaVers 会自动创建以下三张表:

  • jv_commit:存储每次提交的元信息(作者、时间、ID)
  • jv_snapshot:存储实体快照
  • jv_global_id:存储实体全局 ID 映射

⚠️ 踩坑提示:如果你使用的是非默认数据源,或需要自定义表名前缀,可通过配置 javers.sql.schemajavers.sql.table-prefix 调整。

3.3. JaVers 配置属性

JaVers 支持通过 application.ymlapplication.properties 进行配置。大多数场景使用默认值即可。

本文仅启用一个常用配置:记录新创建对象的快照。

javers.newObjectSnapshot=true

其他常见配置项:

属性 说明
javers.commitAuthor 默认提交作者(若未实现 AuthorProvider
javers.prettyPrint JSON 输出是否格式化
javers.mappingStyle 字段映射策略(FIELD/BEAN)

3.4. 领域模型配置

JaVers 内部将对象分为五种类型,每种类型使用不同的差异计算策略:

  • Entity:有唯一标识的聚合根
  • ValueObject:嵌套对象,无独立 ID
  • Value:不可变值类型(如 String、LocalDate)
  • Container:集合类型
  • Primitive:基本类型

如何让 JaVers 正确识别类型?

  1. 显式声明:使用 @Entity@ValueObject 等注解
  2. 隐式推断:JaVers 根据类关系自动判断
  3. 默认策略:所有类默认视为 ValueObject

✅ 优势:JaVers 兼容 JPA 注解!这意味着你可以直接用 @Entity@Embeddable,无需引入 JaVers 特有注解。

例如:

@Entity
public class Store {
    @Id
    private int id;
    private String name;

    @Embedded
    private Address address;

    // ...
}

@Embeddable
public class Address {
    private String address;
    private Integer zipCode;
}

JaVers 会自动将:

  • @Entity@org.javers.core.metamodel.annotation.Entity
  • @Embeddable@ValueObject

干净利落,无需额外工作。

4. 示例项目

我们构建一个简单的商店管理系统,包含 StoreProduct 两个实体,演示审计功能。

4.1. 领域模型

@Entity
public class Store {
    @Id
    @GeneratedValue
    private int id;
    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Product> products = new ArrayList<>();

    // 构造器、getter/setter 省略
}

嵌入式地址:

@Embeddable
public class Address {
    private String address;
    private Integer zipCode;
}

产品实体:

@Entity
public class Product {
    @Id
    @GeneratedValue
    private int id;
    private String name;
    private Double price;

    @ManyToOne
    private Store store;

    // ...
}

4.2. 数据仓库配置

JaVers 提供 @JaversSpringDataAuditable 注解,用于标记需要审计的 Spring Data 接口。

@JaversSpringDataAuditable
public interface StoreRepository extends CrudRepository<Store, Integer> {
}

✅ 只要调用该接口的 save()delete() 等方法,JaVers 就会自动记录变更。

ProductRepository 未加注解:

public interface ProductRepository extends CrudRepository<Product, Integer> {
}

但注意:只要 ProductStore 的关联对象,且随 Store 一起保存,JaVers 仍会审计其变更。这是聚合根的天然行为。

如果你不希望审计某些字段,可用 @DiffIgnore

@DiffIgnore
private List<Product> products;

此外,对于非 Spring Data 场景,可用方法级注解 @JaversAuditable

@JaversAuditable
public void saveProduct(Product product) {
    productRepository.save(product);
}

甚至可以直接加在 Repository 方法上:

public interface ProductRepository extends CrudRepository<Product, Integer> {
    @Override
    @JaversAuditable
    <S extends Product> S save(S s);
}

4.3. 作者提供器(Author Provider)

每次提交都应有明确的作者。JaVers 原生支持 Spring Security,可自动获取当前登录用户。

本例中我们实现一个简单的自定义提供器:

private static class SimpleAuthorProvider implements AuthorProvider {
    @Override
    public String provide() {
        return "zhangsan@company.com";
    }
}

然后通过 @Bean 覆盖默认实现:

@Bean
public AuthorProvider provideJaversAuthor() {
    return new SimpleAuthorProvider();
}

这样每次提交的 author 字段就是 zhangsan@company.com

5. 审计功能实战

启动应用后,JaVers 已开始默默记录每一次变更。我们通过 REST 接口来验证效果。

5.1. 初始提交(INITIAL Commit)

对象首次创建时,JaVers 会生成一个 INITIAL 类型的提交。

我们通过监听 ApplicationReadyEvent 初始化数据:

@EventListener
public void appReady(ApplicationReadyEvent event) {
    Store store = new Store("Tech Store", new Address("Changan Street", 100001));
    for (int i = 1; i < 3; i++) {
        Product product = new Product("Laptop #" + i, 5000.0 * i);
        store.addProduct(product);
    }
    storeRepository.save(store);
}

查询 Store 的快照:

@GetMapping("/stores/snapshots")
public String getStoresSnapshots() {
    QueryBuilder jqlQuery = QueryBuilder.byClass(Store.class);
    List<CdoSnapshot> snapshots = javers.findSnapshots(jqlQuery.build());
    return javers.getJsonConverter().toJson(snapshots);
}

返回结果示例:

[
  {
    "commitMetadata": {
      "author": "zhangsan@company.com",
      "commitDate": "2023-10-01T10:00:00.000",
      "id": 1.00
    },
    "globalId": {
      "entity": "com.example.domain.Store",
      "cdoId": 1
    },
    "state": {
      "name": "Tech Store",
      "address": {
        "valueObject": "com.example.domain.Address",
        "ownerId": { "entity": "com.example.domain.Store", "cdoId": 1 },
        "fragment": "address"
      },
      "products": [
        { "entity": "com.example.domain.Product", "cdoId": 2 },
        { "entity": "com.example.domain.Product", "cdoId": 3 }
      ]
    },
    "type": "INITIAL"
  }
]

✅ 注意:尽管 ProductRepository 未标注 @JaversSpringDataAuditable,但 Product 作为 Store 的子对象,依然被完整记录。

5.2. 更新提交(UPDATE Commit)

当对象状态发生变化时,JaVers 生成 UPDATE 类型提交。

例如更新商店名称并重命名所有商品:

public void rebrandStore(int storeId, String newName) {
    Optional<Store> storeOpt = storeRepository.findById(storeId);
    storeOpt.ifPresent(store -> {
        store.setName(newName);
        store.getProducts().forEach(p -> p.setNamePrefix(newName));
        storeRepository.save(store);
    });
}

执行后查看日志:

INFO  org.javers.core.Javers - Commit(id:2.0, snapshots:3, author:zhangsan@company.com, changes - ValueChange:3)

说明本次提交包含 3 个快照(1 个 Store + 2 个 Product),3 处字段变更。

查询 Product 快照,可见 UPDATE 类型提交:

{
  "commitMetadata": { "id": 2.00, "author": "zhangsan@company.com" },
  "globalId": { "entity": "Product", "cdoId": 2 },
  "state": { "name": "Tech Store - Laptop #1", "price": 5000.0 },
  "type": "UPDATE"
}

⚠️ 重点:JaVers 复用当前事务的数据库连接,审计数据与业务数据在同一次事务中提交或回滚,保证一致性。

5.3. 变更记录(Changes)

JaVers 的核心是“差异计算”。它不直接存储变更,而是通过对比快照动态生成 Change 对象。

例如修改商品价格:

public void updateProductPrice(Integer productId, Double newPrice) {
    productRepository.findById(productId)
        .ifPresent(p -> {
            p.setPrice(newPrice);
            productRepository.save(p);
        });
}

查询变更记录:

@GetMapping("/products/{productId}/changes")
public String getProductChanges(@PathVariable int productId) {
    Product product = productRepository.findById(productId).orElse(null);
    QueryBuilder jqlQuery = QueryBuilder.byInstance(product);
    Changes changes = javers.findChanges(jqlQuery.build());
    return javers.getJsonConverter().toJson(changes);
}

返回结果:

[
  {
    "changeType": "ValueChange",
    "property": "price",
    "left": 5000.0,
    "right": 5500.0,
    "commitMetadata": { "id": 3.00 }
  }
]
  • left:变更前值
  • right:变更后值
  • property:变更字段

简单粗暴,一目了然。

5.4. 影子对象(Shadows)

Shadow 是 JaVers 提供的一种“恢复视图”,可从快照中重建对象在某一时刻的状态,类似“时间机器”。

支持四种查询范围:

范围 说明
Shallow 仅当前实体
Child-value-object 包含嵌套 ValueObject
Commit-deep 包含本次提交关联的所有对象
Deep+ 尝试恢复完整对象图

示例:查询包含地址的商店状态

@GetMapping("/stores/{storeId}/shadows")
public String getStoreShadows(@PathVariable int storeId) {
    Store store = storeService.findStoreById(storeId);
    JqlQuery query = QueryBuilder.byInstance(store)
                    .withChildValueObjects()
                    .build();
    List<Shadow<Store>> shadows = javers.findShadows(query);
    return javers.getJsonConverter().toJson(shadows.get(0));
}

返回:

{
  "it": {
    "id": 1,
    "name": "Tech Store",
    "address": {
      "address": "Changan Street",
      "zipCode": 100001
    },
    "products": []
  },
  "commitMetadata": { "id": 1.00 }
}

⚠️ 注意:products 为空,因为 Address 是 ValueObject,而 Product 是 Entity。若要包含 Product,需使用 withScopeCommitDeep()

6. 总结

JaVers 与 Spring Boot + Spring Data 集成极为顺畅,几乎零配置即可实现完整的数据审计能力

核心优势:

  • ✅ 无缝支持 JPA 注解,无需改造现有模型
  • ✅ 自动追踪聚合根及其关联对象
  • ✅ 提供快照、变更、影子三种视图,满足不同场景
  • ✅ 事务安全,与业务数据强一致

无论是用于调试、合规审计,还是构建操作日志页面,JaVers 都是一个值得加入技术栈的实用工具。

示例代码已托管至 GitHub:https://github.com/example/spring-javers-demo


原始标题:Using JaVers for Data Model Auditing in Spring Data