JPA,Hibernate及SpringDataJPA 的审计

1.概述

在ORM的框架中,数据库审计一般是指跟踪和记录与持久化实体相关的事件,或者仅仅是实体版本跟踪。受SQL触发器的启发,事件是对实体的插入,更新和删除操作,类似于源码版本控制。

我们将演示三种不同的审计方法。首先,我们将使用标准JPA实现它,接下来,我们将看两个提供自己的审计功能的JPA扩展:一个由Hibernate提供,另一个由Spring Data提供。

以下是将在此示例中使用的示例相关实体BarFoo

upload successful

2. 使用JPA进行审计

JPA没有明确包含审计API,但可以使用实体生命周期事件来实现功能。

2.1 @PrePersist@PreUpdate@PreRemove

在JPA Entity类中,实体对象生命周期在发生变化时会有相应的事件回调,比如相应的DML操作之前执行的回调,有@PrePersist@PreUpdate@PreRemove 分别对于 持久化前更新前以及删除前

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Bar {

@PrePersist
public void onPrePersist() { ... }

@PreUpdate
public void onPreUpdate() { ... }

@PreRemove
public void onPreRemove() { ... }

}

这些内部回调方法没有返回值,且没有参数,它们可以具有任何名称和任何访问级别,但不应该是静态的。

请注意,JPA 中的 @Version 注解与我们的主题不一样 - 它与乐观锁定相关,而不是与审计数据相关。

2.2 实现回调方法

上一节提到的方法有一个限制,JPA 2 specification (JSR 317) 的相关描述如下:

In general, the lifecycle method of a portable application should not invoke EntityManager or Query operations, access other entity instances, or modify relationships within the same persistence context. A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked.

意思就是生命周期回调方法不应该调用 EntityManager 或者 Query 的操作去修改其它的实例或者实例的关联关系。

在没有审计框架的情况下,我们必须手动维护数据库架构和域模型。对于我们的简单用例,让我们向实体添加两个新属性,因为我们只能管理“实体的非关系状态”。operation 属性将存储执行的操作的名称,timestamp属性是操作的时间戳:

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
@Entity
public class Bar {

//...

@Column(name = "operation")
private String operation;

@Column(name = "timestamp")
private long timestamp;

//...

// standard setters and getters for the new properties

//...

@PrePersist
public void onPrePersist() {
audit("INSERT");
}

@PreUpdate
public void onPreUpdate() {
audit("UPDATE");
}

@PreRemove
public void onPreRemove() {
audit("DELETE");
}

private void audit(String operation) {
setOperation(operation);
setTimestamp((new Date()).getTime());
}

}

如果需要将此类审计添加到多个类,则可以使用 @EntityListeners 来集中代码。例如:

1
2
3
4
5
6
7
8
9
10
11
@EntityListeners(AuditListener.class)
@Entity
public class Bar { ... }

public class AuditListener {

@PrePersist
private void beforePersist(Object object) { ... }

...
}

3. Hibernate Envers

如果使用 Hibernate 框架,我们可以使用InterceptorEventListeners 以及数据库触发器来完成审计。但是ORM框架提供了Envers,这是一个实现持久化类的审计和版本控制的模块。

3.1 Envers入门

要启用Envers,需要添加hibernate-envers 依赖:

1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate.version}</version>
</dependency>

然后在@Entity或特定的@Column上添加@Audited注释(如果您只需要审计特定属性):

1
2
3
@Entity
@Audited
public class Bar { ... }

请注意,BarFoo有一对多的关系。在这种情况下,我们要么通过在Foo上添加@Audited或在Bar的关系属性上设置@NotAudited来审计Foo

1
2
3
@OneToMany(mappedBy = "bar")
@NotAudited
private Set<Foo> fooSet;

3.2 创建审计日志表

有几种方法可以创建审计表:

  • 设置hibernate.hbm2ddl.auto=create, create-drop or update,这样Envers就可以自动创建它们
  • 使用o rg.hibernate.tool.EnversSchemaGenerator以编程方式导出完整的数据库Schema
  • 使用Ant任务生成适当的DDL语句
  • 使用Maven插件从映射(例如Juplo)生成数据库模式以导出Envers模式(与Hibernate 4及更高版本一起使用)

注意使用hibernate.hbm2ddl.auto在生产中是不安全的。

在我们的例子中,bar_AUDfoo_AUD(如果你将Foo设置为@Audited)表应该自动生成。审计表从实体表中复制所有审计字段,其中包含两个字段REVTYPE(值为:“0”表示添加,“1”表示更新,“2”表示删除实体)和REV

除此之外,默认会生成一个名为REVINFO的表,它包括两个重要的字段REVREVTSTMP,记录每次修改的时间戳。正如你猜测的那样,bar_AUD.REVfoo_AUD.REV实际上是REVINFO.REV的外键。

3.3 配置Envers

您可以像任何其他Hibernate属性一样配置Envers属性。例如,让我们将审计表后缀(默认为“ _AUD ”)更改为“ _AUDIT_LOG ”。比如设置相应属性org.hibernate.envers.audit_table_suffix的值:

1
2
3
4
Properties hibernateProperties = new Properties(); 
hibernateProperties.setProperty(
"org.hibernate.envers.audit_table_suffix", "_AUDIT_LOG");
sessionFactory.setHibernateProperties(hibernateProperties);

Envers文档中找到可用属性的完整列表。

3.4 访问实体历史记录

您可以通过类似于通过Hibernate条件API查询数据的方式查询历史数据。实体的审计历史记录可以使用AuditReader 接口访问,这个接口可以通过 的EntityManagerSession 获取,如下所示:

1
AuditReader reader = AuditReaderFactory.get(session);

Envers提供AuditQueryCreator(由AuditReader.createQuery()返回)创建特定于审计的查询,如以所示将返回在修订版#2中修改的所有Bar实例(其中bar_AUDIT_LOG.REV = 2):

1
AuditQuery query = reader.createQuery().forEntitiesAtRevision(Bar.class, 2)

以下是如何查询Bar的修订版,即它将导致获取所有审计状态中所有Bar实例的列表:

1
2

AuditQuery query = reader.createQuery().forRevisionsOfEntity(Bar.class, true, true);

如果第二个参数为false,则结果还会 join REVINFO表,否则,仅返回实体实例。最后一个参数指定是否返回已删除的Bar实例。

还可以使用 AuditEntity 工厂类指定约束:

1
query.addOrder(AuditEntity.revisionNumber().desc());

4. Spring Data JPA

Spring Data JPA 是一个通过在JPA提供程序的顶部添加额外的抽象层来扩展JPA的框架。我们可以扩展CrudRepository <T,ID extends Serializable>,用于通用CRUD操作的接口。一旦您创建了 Repository 并将其注入另一个组件,Spring Data就会自动提供实现,您就可以添加审计功能了。

4.1 启用JPA审计

只需在 @Configuration 类上添加 @EnableJpaAuditing 即可启用 JPA 审计,如下所示:

1
2
3
4
5
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EnableJpaAuditing
public class PersistenceConfig { ... }

4.2 添加Spring的实体回调监听器

我们已经知道,JPA提供@EntityListeners 注释来指定回调监听类。Spring Data 也提供了自己的JPA实体监听器类:AuditingEntityListener。所以让我们为Bar 实体指定监听器,如下所示:

1
2
3
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar { ... }

现在,监听器将在持久化和更新Bar实体时捕获审计信息。

4.3 跟踪创建的和上次修改的日期

接下来,我们将添加两个新属性,用于将创建和最后修改日期存储到Bar实体。这些属性由@CreatedDate@LastModifiedDate 进行注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {

//...

@Column(name = "created_date", nullable = false, updatable = false)
@CreatedDate
private long createdDate;

@Column(name = "modified_date")
@LastModifiedDate
private long modifiedDate;

//...

}

通常,可以将这几个属性移动到基类(由@MappedSuperClass注释),该基类将由所有审计实体继承。在我们的示例中,为简单起见,我们将它们直接添加到Bar

4.4 审计更改的作者

您不仅可以跟踪何时进行更改,还可以跟踪谁进行了修改,同样的,添加两个属性,并由 @CreatedBy@LastModifiedBy 进行注解:

1
2
3
4
5
6
7
8
9
10
11
12

//...

@Column(name = "created_by")
@CreatedBy
private String createdBy;

@Column(name = "modified_by")
@LastModifiedBy
private String modifiedBy;

//

使用@CreatedBy@LastModifiedBy注释的列将自动填充创建或上次修改实体的作者。当然,该作者信息需要开发者自己提供。实际上框架是会查找 Spring 上下文中的 AuditorAware 接口实现才,并调用其 getCurrentAuditor() 方法,定义如下:

1
2
3
4
5
6
7
8
public class AuditorAwareImpl implements AuditorAware<String> {

@Override
public String getCurrentAuditor() {
// your custom logic
}

}

根据不同的情况,可以从 Session 或者 SpringSecurity 框架的 Principal 中获取当前登录用户信息

为了配置该应用使用AuditorAwareImpl查找当前主体,@EnableJpaAuditing 中可以设置 auditorAwareRef属性为我们前面提到的 AuditorAware 实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableJpaAuditing(auditorAwareRef="auditorProvider")
public class PersistenceConfig {

//...

@Bean
AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}

//...

}