Spring和Hibernate的自定义审计

1. 概述

通常我们在应用中会需要对于数据库的操作进行日志的审计,以保存当时的数据操作者及时间等相关信息,为今后的数据变化跟踪作铺垫,在需要时可以追踪到具体修改数据的人。这个时候可以使用Enversspring数据jpa审计,我的这篇博客记录了一些常用的JPA,Hibernate及SpringDataJPA 的审计方案。如果由于某些原因你不能使用Envers和Spring JPA的审计,那么还有一个方法,就是实现与hibernate事件监听器,并和spring事务资源同步进行绑定,然后在事务提交的时候进行处理。

2. 事件监听

首先,从事件监听器开始。捕获所有插入,更新和删除操作。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Component
public class AuditLogEventListener
implements PostUpdateEventListener, PostInsertEventListener, PostDeleteEventListener {

@Override
public void onPostDelete(PostDeleteEvent event) {
AuditedEntity audited = event.getEntity().getClass().getAnnotation(AuditedEntity.class);
if (audited != null) {
AuditLogServiceData.getHibernateEvents().add(event);
}
}

@Override
public void onPostInsert(PostInsertEvent event) {
AuditedEntity audited = event.getEntity().getClass().getAnnotation(AuditedEntity.class);
if (audited != null) {
AuditLogServiceData.getHibernateEvents().add(event);
}
}

@Override
public void onPostUpdate(PostUpdateEvent event) {
AuditedEntity audited = event.getEntity().getClass().getAnnotation(AuditedEntity.class);
if (audited != null) {
AuditLogServiceData.getHibernateEvents().add(event);
}
}

@Override
public boolean requiresPostCommitHanding(EntityPersister persister) {
return true; // Envers sets this to true only if the entity is versioned. So figure out for yourself if that's needed
}
}

请注意AuditedEntity- 它是一个自定义标记注解(retention = runtime,target = type),注解在实体类上,用来标识这个实体是需要被审计的。

3. 事务绑定

同时,在捕获到这些事件后,将事件信息填充到 AuditLogServiceData 中去,AuditLogServiceData 是我们自己定义的一个数据结构,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AuditLogServiceData {
private static final String HIBERNATE_EVENTS = "hibernateEvents";
@SuppressWarnings("unchecked")
public static List<Object> getHibernateEvents() {
if (!TransactionSynchronizationManager.hasResource(HIBERNATE_EVENTS)) {
TransactionSynchronizationManager.bindResource(HIBERNATE_EVENTS, new ArrayList<>());
}
return (List<Object>) TransactionSynchronizationManager.getResource(HIBERNATE_EVENTS);
}

public static Long getActorId() {
return (Long) TransactionSynchronizationManager.getResource(AUDIT_LOG_ACTOR);
}

public static void setActor(Long value) {
if (value != null) {
TransactionSynchronizationManager.bindResource(AUDIT_LOG_ACTOR, value);
}
}

public void clear() {
// unbind all resources
}
}

除了存储事件之外,我们还需要存储正在执行操作的用户。为了得到它,我们需要提供一个方法参数级注解来指定一个参数。我的案例中的注解被称为AuditLogActor(retention = runtime,type = parameter)。

这里模拟了 OpenSessionInViewInterceptor 的实现方式,将这些事件及操作者信息绑定到当前的事务上,这样就可以在进行事务提交的时候获取到这些事件及操作者的相关信息,并填充到审计字段上。

4. 事件拦截与审计处理

现在剩下的是处理事件的代码。我们希望在提交当前事务之前执行此操作。如果事务在提交时失败,则审计条目插入也将失败。我们用一点AOP来做到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Aspect
@Component
class AuditLogStoringAspect extends TransactionSynchronizationAdapter {

@Autowired
private ApplicationContext ctx;

@Before("execution(* *.*(..)) && @annotation(transactional)")
public void registerTransactionSyncrhonization(JoinPoint jp, Transactional transactional) {
Logger.log(this).debug("Registering audit log tx callback");
TransactionSynchronizationManager.registerSynchronization(this);
MethodSignature signature = (MethodSignature) jp.getSignature();
int paramIdx = 0;
// 获取参数列表中注解了 AuditLogActor 的参数,来作为操作者信息保存在 AuditLogServiceData 中
for (Parameter param : signature.getMethod().getParameters()) {
if (param.isAnnotationPresent(AuditLogActor.class)) {
AuditLogServiceData.setActor((Long) jp.getArgs()[paramIdx]);
}
paramIdx ++;
}
}

@Override
public void beforeCommit(boolean readOnly) {
Logger.log(this).debug("tx callback invoked. Readonly= " + readOnly);
if (readOnly) {
return;
}
for (Object event : AuditLogServiceData.getHibernateEvents()) {
// 这里就是遍历之前捕获的所有的 Hibernate 事件,
// 并根据需要进行审计处理,比如日志、DB存储等
}
}

@Override
public void afterCompletion(int status) {
// we have to unbind all resources as spring does not do that automatically
AuditLogServiceData.clear();
}
}

我们采用实现 TransactionSynchronizationAdapter 的方式来拦截事务的处理过程,在事务提交前对当前的操作进行审计,代码中的注解已经说明得很清楚了。

5. 实际的调用

如同第2节撰述,首先我们的 FooBar 实体需要添加 AuditedEntity注解,然后在我们进行保存的地方添加我们的 @AuditLogActor 来标记我们当前的操作者信息,如下所示:

1
2
@Transactional
public void saveFoo(FooBar foobar, @AuditLogActor Long actorId) { .. }

6. 总结

总结一下:整个过程其实就是监听hibernate事件,将所有insert,update和delete事件存储为spring事务同步资源,然后通过 AOP 注册 Spring 事务 “callback”,该事务在每个事务提交之前调用,处理所有事件并插入相应的审核日志条目。

当然,这个方案还是比较烦琐和有一定的局限性的,比如需要在各个事务操作的地方传递 @AuditLogActor ,所以如第1章所述,我们还是应该尽量使用Enversspring数据jpa审计,只有在这两个方案不适用时才考虑使用本文介绍的案例。