
在企业级应用中,对关键配置和业务数据的变更进行审计追踪是一个普遍且重要的需求。无论是金融系统的资金流水、电商平台的价格调整,还是配置管理中心的参数修改,我们都迫切需要清晰地回答以下几个核心问题:谁在什么时间修改了什么数据,以及具体的变更内容是什么。
传统手工审计方案的痛点
最直观的实现方式是在每个业务方法中手动插入审计日志记录代码。
public void updatePrice(Long productId, BigDecimal newPrice) {
Product old = productRepository.findById(productId).get();
productRepository.updatePrice(productId, newPrice);
// 手动记录变更
auditService.save("价格从 " + old.getPrice() + " 改为 " + newPrice);
}
这种方式在项目初期尚可应付,但随着业务复杂度的提升,其弊端会日益凸显:
- 代码重复:大量相似的审计逻辑散落在各个业务方法中。
- 维护困难:当业务实体字段发生变更时,需要同步修改多处审计代码。
- 格式混乱:不同开发者记录的日志格式难以统一,不利于后续分析。
- 查询不便:基于字符串拼接的日志难以进行结构化的查询与统计。
- 耦合严重:审计逻辑与核心业务逻辑高度耦合,污染了业务代码的纯粹性。
这些痛点在实际运维中常表现为:产品价格被错误修改后需要耗费大量时间排查责任人;关键配置误删后因缺乏详尽的变更记录而无法恢复;为满足越来越严格的合规审计要求,不得不对已有系统进行大规模重构。
核心需求分析
基于以上问题,一个理想的自动化审计系统应具备以下特性:
- 零侵入性:业务代码无需感知和嵌入任何审计逻辑。
- 自动化:通过声明式配置(如注解)即可自动启用审计功能。
- 精确比对:能够记录字段级别的数据变更详情。
- 结构化存储:审计日志应以结构化方式(如JSON)存储,便于查询、分析和可视化。
- 信息完整:日志需包含操作人、时间、操作类型、实体ID等完整的元数据。
在技术选型上,我们选择使用 Javers 作为核心比对组件。它是一款专业的Java对象差异比对库,具备成熟的算法,与 Spring Boot 集成简单,支持多种存储后端,并且其输出的JSON格式非常友好。
系统架构与设计思路
我们采用 AOP(面向切面编程) 结合自定义注解的设计模式来实现解耦和自动化。
┌─────────────────┐
│ Controller │
└─────────┬───────┘
│ AOP 拦截
┌─────────▼───────┐
│ Service │ ← 核心业务逻辑保持不变
└─────────┬───────┘
│
┌─────────▼───────┐
│ AuditAspect │ ← 统一处理审计逻辑的切面
└─────────┬───────┘
│
┌─────────▼───────┐
│ Javers Core │ ← 对象差异比对引擎
└─────────┬───────┘
│
┌─────────▼───────┐
│ Audit Storage │ ← 结构化存储(内存/DB)
└─────────────────┘
核心设计点:
- 注解驱动:使用
@Audit 注解标记需要被审计的业务方法。
- 切面拦截:利用Spring AOP自动拦截所有带
@Audit 注解的方法执行。
- 差异比对:在方法执行前后,使用Javers对业务对象的状态进行比对,生成结构化差异。
- 统一存储:将包含完整变更信息的审计日志进行统一的持久化存储。
核心代码实现
1. 项目依赖 (pom.xml)
首先,在Spring Boot项目中引入必要的依赖。
<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>
2. 自定义审计注解 (@Audit)
通过此注解来声明审计行为。
@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;
// 操作人参数名
String actorParam() default "";
// 实体参数在方法参数列表中的位置
int entityIndex() default 0;
enum ActionType {
CREATE, UPDATE, DELETE, AUTO
}
}
3. 审计切面实现 (AuditAspect)
这是整个方案的核心,负责统一的审计逻辑处理。
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AuditAspect {
private final Javers javers;
// 使用内存存储审计日志(生产环境建议接入数据库,如使用Javers的SQL仓库)
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();
// 1. 提取实体ID
String entityId = extractEntityId(args, paramNames, auditAnnotation);
if (entityId == null) {
log.warn("无法提取实体ID,跳过审计: {}", method.getName());
return joinPoint.proceed();
}
// 2. 提取实体对象
Object entity = null;
if (auditAnnotation.entityIndex() >= 0 && auditAnnotation.entityIndex() < args.length) {
entity = args[auditAnnotation.entityIndex()];
}
// 3. 提取操作人
String actor = extractActor(args, paramNames, auditAnnotation);
// 4. 确定操作类型
Audit.ActionType actionType = determineActionType(auditAnnotation, method.getName());
// 5. 获取执行前的对象快照
Object beforeSnapshot = dataStore.get(buildKey(entityId));
// 6. 执行原业务方法
Object result = joinPoint.proceed();
// 7. 确定执行后的对象快照
Object afterSnapshot = determineAfterSnapshot(entity, actionType);
// 8. 使用Javers比对差异并记录审计日志
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) // 结构化差异信息
);
}
// 9. 更新数据快照存储
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;
}
// 其他辅助方法(extractActor, determineActionType, recordAudit等)...
}
4. 业务服务层示例
业务代码保持简洁,仅通过注解声明审计需求。
@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;
}
}
5. 审计日志实体与Javers配置
定义结构化的审计日志记录,并配置Javers。
// 审计日志记录
public record AuditLog(
String id,
String entityType,
String entityId,
String action,
String actor,
Instant occurredAt,
String diffJson // 存储Javers生成的JSON差异
) {}
// Javers配置
@Configuration
public class JaversConfig {
@Bean
public Javers javers() {
return JaversBuilder.javers()
.withPrettyPrint(true)
.build();
}
}
应用场景与日志示例
场景1:更新产品价格
- 请求:
PUT /api/products/prod-001,将价格从100.00改为99.99。
- 审计日志:
{
"entityType": "Product",
"entityId": "prod-001",
"action": "UPDATE",
"actor": "张三",
"diffJson": "{\"changes\":[{\"property\":\"price\",\"left\":100.00,\"right\":99.99}]}"
}
场景2:查询完整操作历史
- 请求:
GET /api/products/prod-001/audits
- 响应:返回该产品所有的创建、更新、删除操作记录列表,每条记录都包含结构化的变更详情。
场景3:删除数据
- 请求:
DELETE /api/products/prod-001
- 审计日志:会记录一条操作类型为
DELETE的日志,尽管diffJson可能为空,但关键的操作行为已被追踪。
通过 Javers + Spring AOP + 自定义注解 的组合,我们成功构建了一个对企业级业务逻辑零侵入的自动化数据变更审计方案。该方案显著提升了开发效率,将审计逻辑集中化管理,降低了维护成本,并通过结构化的日志输出极大地改善了数据可追溯性。
本文示例完整代码仓库:https://github.com/yuboon/java-examples/tree/master/springboot-object-version