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.schema
和 javers.sql.table-prefix
调整。
3.3. JaVers 配置属性
JaVers 支持通过 application.yml
或 application.properties
进行配置。大多数场景使用默认值即可。
本文仅启用一个常用配置:记录新创建对象的快照。
javers.newObjectSnapshot=true
其他常见配置项:
属性 | 说明 |
---|---|
javers.commitAuthor |
默认提交作者(若未实现 AuthorProvider ) |
javers.prettyPrint |
JSON 输出是否格式化 |
javers.mappingStyle |
字段映射策略(FIELD/BEAN) |
3.4. 领域模型配置
JaVers 内部将对象分为五种类型,每种类型使用不同的差异计算策略:
Entity
:有唯一标识的聚合根ValueObject
:嵌套对象,无独立 IDValue
:不可变值类型(如 String、LocalDate)Container
:集合类型Primitive
:基本类型
如何让 JaVers 正确识别类型?
- 显式声明:使用
@Entity
、@ValueObject
等注解 - 隐式推断:JaVers 根据类关系自动判断
- 默认策略:所有类默认视为
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. 示例项目
我们构建一个简单的商店管理系统,包含 Store
和 Product
两个实体,演示审计功能。
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> {
}
但注意:只要 Product
是 Store
的关联对象,且随 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