
技术选型与依赖后端JavaJaVers 版本7.9.0Maven 依赖在父 POM 的dependencyManagement中声明版本properties javers.version7.9.0/javers.version /properties dependencyManagement dependencies dependency groupIdorg.javers/groupId artifactIdjavers-spring-boot-starter-sql/artifactId version${javers.version}/version /dependency /dependencies /dependencyManagement在 common 模块中引入所有业务模块依赖 commondependency groupIdorg.javers/groupId artifactIdjavers-spring-boot-starter-sql/artifactId /dependency说明使用javers-spring-boot-starter-sql而非javers-spring-boot-starter-mongo。JaVers 的快照数据存储在应用同一 MySQL 数据库中无需额外数据库。前端Vue 3无需额外依赖使用 Element Plus 的el-table、el-scrollbar、el-checkbox、el-empty组件即可。2. JaVers 配置application.ymljavers: spring-data: enabled: false # 不使用 JaversSpringDataAuditable 自动审计 sql: enabled: true # 显式指定使用应用的 EntityManagerFactory确保 JaVers 与应用共享事务 entity-manager-bean-name: entityManagerFactory关键说明spring-data.enabled: false我们不使用JaversSpringDataAuditable注解。这种自动审计方式会导致每次 Spring Data JPA 保存时都自动提交快照粒度太粗且无法使用 DTO 包装。sql.enabled: true使用 SQL 存储MySQL。entity-manager-bean-name必须指定确保 JaVers 的快照提交在应用的同一事务中。如果 JaVers 提交失败整个业务操作回滚。JaVers 自动创建的表启动应用后JaVers 会在数据库中自动创建以下表通过 JPAddl-autoupdate或 JaVers 内置的 Liquibase 脚本表名用途javers_commit每次快照提交的元数据作者、时间、提交IDjavers_snapshot实体在每个版本号下的完整状态JSON 序列化javers_global_id全局对象标识类型名 实体ID 的映射3. 核心基础设施事件驱动架构3.1 设计理念不使用 JaVers 自带的JaversSpringDataAuditable而是通过自定义 Spring 事件机制手动触发快照提交。这样做的优势精确控制提交时机只在业务真正完成时提交不在中间状态提交支持 DTO 包装可以提交自定义的 SnapshotDTO包含关联数据而非原始 Entity统一事件入口所有实体的变更通过同一事件通道方便后续扩展如通知、日志等同步事务保证事件监听器是同步的快照提交与业务操作在同一事务中3.2 EntityChangeEvent —— 事件类package your.project.common.event; import lombok.Getter; import org.springframework.context.ApplicationEvent; Getter public class EntityChangeEvent extends ApplicationEvent { public enum ChangeType { CREATED, // 新增 UPDATED, // 更新 DELETED // 删除 } private final ChangeType changeType; private final Object entity; // 要提交给 JaVers 的对象可以是 Entity 或 DTO private final String author; // 操作人标识 public EntityChangeEvent(Object source, ChangeType changeType, Object entity, String author) { super(source); this.changeType changeType; this.entity entity; this.author author; } /** 通过反射获取实体 ID */ public Long getEntityId() { if (entity ! null) { try { var method entity.getClass().getMethod(getId); return (Long) method.invoke(entity); } catch (Exception e) { return null; } } return null; } /** 获取实体类型名称 */ public String getEntityType() { return entity ! null ? entity.getClass().getSimpleName() : null; } }3.3 EntityChangeEventPublisher —— 事件发布器package your.project.common.event; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; Component RequiredArgsConstructor public class EntityChangeEventPublisher { private static final String DEFAULT_AUTHOR system; private final ApplicationEventPublisher publisher; /** 获取当前登录用户标识。 * 你需要替换为你们项目中获取当前用户的方法 */ private String getCurrentAuthor() { // 示例从 SecurityContext 或自定义 ThreadLocal 获取 // return SecurityContextHolder.getContext().getAuthentication().getName(); return DEFAULT_AUTHOR; } /** 发布新增事件 */ public T void publishCreated(T entity) { publishCreated(entity, getCurrentAuthor()); } public T void publishCreated(T entity, String author) { publisher.publishEvent(new EntityChangeEvent( this, EntityChangeEvent.ChangeType.CREATED, entity, author)); } /** 发布更新事件 */ public T void publishUpdated(T entity) { publishUpdated(entity, getCurrentAuthor()); } public T void publishUpdated(T entity, String author) { publisher.publishEvent(new EntityChangeEvent( this, EntityChangeEvent.ChangeType.UPDATED, entity, author)); } /** 发布删除事件 */ public T void publishDeleted(T entity) { publishDeleted(entity, getCurrentAuthor()); } public T void publishDeleted(T entity, String author) { publisher.publishEvent(new EntityChangeEvent( this, EntityChangeEvent.ChangeType.DELETED, entity, author)); } }3.4 EntityChangeEventListener —— 事件监听器核心package your.project.common.event; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.javers.core.Javers; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; Component RequiredArgsConstructor Slf4j public class EntityChangeEventListener { private final Javers javers; /** * 同步监听实体变更事件将实体状态提交到 JaVers。 * 同步执行保证快照提交与业务操作在同一数据库事务中。 */ EventListener public void handleEvent(EntityChangeEvent event) { try { switch (event.getChangeType()) { case CREATED, UPDATED, DELETED: // 核心调用将对象提交给 JaVers javers.commit(event.getAuthor(), event.getEntity()); break; } } catch (Exception e) { log.error(记录实体变更审计失败: {}, event, e); throw new RuntimeException(记录实体变更审计失败: e.getMessage()); } } }重要说明监听器不加Async是同步执行的。这意味着 JaVers 的commit()在同一个数据库事务中完成。如果commit()抛出异常会向上传播导致业务操作回滚。javers.commit(author, entity)的两个参数author是操作人标识可以是用户ID或用户名entity是待快照的对象Entity 或 DTO。4. 实体与 DTO 的 JaVers 注解规范4.1 基础实体类TimeEntity —— 所有实体的父类package your.project.common.entity; import jakarta.persistence.*; import lombok.Data; import org.javers.core.metamodel.annotation.DiffIgnore; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; Data MappedSuperclass EntityListeners(AuditingEntityListener.class) public abstract class TimeEntity { Column(updatable false) CreatedDate DiffIgnore // 创建时间不纳入版本对比 private LocalDateTime createTime; Column LastModifiedDate DiffIgnore // 更新时间不纳入版本对比 private LocalDateTime updateTime; }package your.project.common.entity; import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; EqualsAndHashCode(callSuper true) Data MappedSuperclass public abstract class BaseEntity extends TimeEntity { Id GeneratedValue(strategy GenerationType.TABLE, generator id_generator) TableGenerator(name id_generator) private Long id; }4.2 直接追踪 Entity模式A适用于简单实体直接对 Entity 类加 JaVers 注解package your.project.domain.entity; import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; import org.javers.core.metamodel.annotation.PropertyName; import org.javers.core.metamodel.annotation.TypeName; import your.project.common.entity.BaseEntity; Data Entity EqualsAndHashCode(callSuper true) Table(name your_entity) TypeName(YourEntity) // 给 JaVers 看的类型名建议与类名一致 public class YourEntity extends BaseEntity { Column(length 100) PropertyName(名称) // 版本对比时显示的中文名 private String name; Column(length 50) PropertyName(状态) private String status; Column(length 200) PropertyName(描述) private String description; // 不想纳入版本记录的字段加 DiffIgnore DiffIgnore Column(length 50) private String internalCode; }4.3 使用 SnapshotDTO 追踪模式B适用于需要将关联数据也纳入快照的复杂聚合场景。核心思路JaVers 提交的不是原始 Entity而是手动构建的 DTO。Step 1定义 DTO带 JaVers 注解package your.project.domain.dto; import lombok.Data; import org.javers.core.metamodel.annotation.Id; import org.javers.core.metamodel.annotation.PropertyName; import org.javers.core.metamodel.annotation.TypeName; import org.javers.core.metamodel.annotation.DiffIgnore; import java.time.LocalDateTime; import java.util.List; Data TypeName(YourAggregateSnapshotDTO) public class YourAggregateSnapshotDTO { Id // JaVers 的 Id不是 JPA 的 PropertyName(ID) private Long id; PropertyName(名称) private String name; PropertyName(状态) private String status; PropertyName(子项列表) private ListChildSnapshotDTO children; DiffIgnore PropertyName(创建时间) private LocalDateTime createTime; /** * 工厂方法从 Entity 构建 DTO */ public static YourAggregateSnapshotDTO from(YourEntity entity, ListChildEntity children) { YourAggregateSnapshotDTO dto new YourAggregateSnapshotDTO(); dto.setId(entity.getId()); dto.setName(entity.getName()); dto.setStatus(entity.getStatus()); dto.setCreateTime(entity.getCreateTime()); dto.setChildren(children.stream() .map(ChildSnapshotDTO::from) .toList()); return dto; } }Step 2子项用 Value 标注JaVers 的 ValueObjectpackage your.project.domain.dto; import lombok.Value; import org.javers.core.metamodel.annotation.PropertyName; Value // JaVers 的 Value 注解表示这是一个值对象不独立追踪版本 public class ChildSnapshotDTO { PropertyName(子项ID) private Long childId; PropertyName(子项名称) private String childName; public static ChildSnapshotDTO from(ChildEntity entity) { return new ChildSnapshotDTO(entity.getId(), entity.getName()); }