在ORM的上下文中,数据库审计意味着跟踪和记录与持久实体相关的事件,或者只是实体版本控制。受 SQL 触发器的启发,这些事件是对实体的插入、更新和删除操作。数据库审核的好处类似于源版本控制提供的好处。
在本教程中,我们将演示将审核引入应用程序的三种方法。首先,我们将使用标准 JPA 实现它。接下来,我们将看看两个提供自己的审计功能的JPA扩展,一个由Hibernate提供,另一个由Spring Data提供。
下面是我们将在本例中使用的示例相关实体 Bar和Foo:

JPA 没有显式包含审计 API,但我们可以使用实体生命周期事件来实现此功能。
在 JPA 实体类中,我们可以指定一个方法作为回调,我们可以在特定实体生命周期事件期间调用该方法。由于我们对在相应的 DML 操作之前执行的回调感兴趣,因此 @PrePersist、@PreUpdate和 @PreRemove回调注释可用于我们的目的:
- @Entity
- public class Bar {
-
- @PrePersist
- public void onPrePersist() { ... }
-
- @PreUpdate
- public void onPreUpdate() { ... }
-
- @PreRemove
- public void onPreRemove() { ... }
-
- }
Copy
内部回调方法应始终返回void,并且不带任何参数。它们可以具有任何名称和任何访问级别,但不应是静态的。
请注意,JPA 中的@Version注释与我们的主题并不严格相关;它与乐观锁定的关系大于与审计数据的关系。
不过,这种方法存在很大的限制。如 JPA2 规范 (JSR 317) 中所述:
通常,可移植应用程序的生命周期方法不应调用EntityManager或Query操作、访问其他实体实例或修改同一持久性上下文中的关系。生命周期回调方法可以修改调用它的实体的非关系状态。
在没有审计框架的情况下,我们必须手动维护数据库模式和域模型。对于我们的简单用例,让我们向实体添加两个新属性,因为我们只能管理“实体的非关系状态”。操作属性将存储所执行操作的名称,时间戳属性用于操作的时间戳:
- @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());
- }
-
- }
Copy
如果我们需要将这样的审计添加到多个类中,我们可以@EntityListeners来集中代码:
- @EntityListeners(AuditListener.class)
- @Entity
- public class Bar { ... }
Copy
- public class AuditListener {
-
- @PrePersist
- @PreUpdate
- @PreRemove
- private void beforeAnyOperation(Object object) { ... }
-
- }
Copy
使用Hibernate,我们可以利用拦截器和事件侦听器以及数据库触发器来完成审计。但是ORM框架提供了Envers,一个实现持久类的审计和版本控制的模块。
要设置 Envers,我们需要将hibernate-enversJAR 添加到我们的类路径中:
- <dependency>
- <groupId>org.hibernate</groupId>
- <artifactId>hibernate-envers</artifactId>
- <version>${hibernate.version}</version>
- </dependency>
Copy
然后我们添加@Audited注释,要么在 an@Entity(审计整个实体)上,要么在 specific@Columns(如果我们只需要审计特定属性):
- @Entity
- @Audited
- public class Bar { ... }
Copy
请注意,Bar与Foo 之间存在一对多关系。在这种情况下,我们要么还需要审核Foo@Audited在Foo上添加,或者在Bar中对关系的属性进行设置@NotAudited:
- @OneToMany(mappedBy = "bar")
- @NotAudited
- private Set
fooSet;
Copy
有几种方法可以创建审核表:
我们将采用第一条路线,因为它是最直接的,但请注意,使用hibernate.hbm2ddl.auto在生产中并不安全。
在我们的例子中,bar_AUD和foo_AUD(如果我们也将Foo设置为@Audited)表应该自动生成。审核表使用两个字段从实体的表中复制所有审核字段,即 REVTYPE(值为:“0”表示添加,“1”表示更新,“2”表示删除实体)和REV。
除此之外,默认情况下将生成一个名为REVINFO的额外表。它包括两个重要字段,REV和REVTSTMP,并记录每个修订的时间戳。我们可以猜到,bar_AUD。修订和foo_AUD。REV实际上是REVINFO.REV 的外键。
我们可以像配置任何其他 Hibernate 属性一样配置 Envers 属性。
例如,让我们将审计表后缀(默认为“_AUD”)更改为“_AUDIT_LOG”。以下是我们如何设置相应属性org.hibernate.envers.audit_table_suffix的值:
- Properties hibernateProperties = new Properties();
- hibernateProperties.setProperty(
- "org.hibernate.envers.audit_table_suffix", "_AUDIT_LOG");
- sessionFactory.setHibernateProperties(hibernateProperties);
Copy
可用属性的完整列表可以在Envers 文档中找到。
我们可以以类似于通过休眠条件 API 查询数据的方式查询历史数据。我们可以使用AuditReader界面访问实体的审计历史记录,我们可以通过打开的EntityManager或会话通过AuditReaderFactory 获取:
AuditReader reader = AuditReaderFactory.get(session);Copy
Envers 提供AuditQueryCreator(由AuditReader.createQuery() 返回)以创建特定于审计的查询。以下行将返回在修订版 #2 修改的所有柱线实例(其中bar_AUDIT_LOG。修订版 = 2):
- AuditQuery query = reader.createQuery()
- .forEntitiesAtRevision(Bar.class, 2)
Copy
以下是我们如何查询Bar 的修订版。这将导致获取所有状态的所有已审核Bar实例的列表:
- AuditQuery query = reader.createQuery()
- .forRevisionsOfEntity(Bar.class, true, true);
Copy
如果第二个参数为 false,则结果与REVINFO表联接。否则,仅返回实体实例。最后一个参数指定是否返回已删除的 Bar实例。
然后,我们可以使用AuditEntity工厂类指定约束:
query.addOrder(AuditEntity.revisionNumber().desc());Copy
Spring Data JPA是一个框架,通过在JPA提供程序的顶部添加额外的抽象层来扩展JPA。该层支持通过扩展 Spring JPA 存储库接口来创建 JPA 存储库。
出于我们的目的,我们可以扩展CrudRepository
首先,我们希望通过注释配置启用审核。为了做到这一点,我们在@Configuration类中添加@EnableJpaAuditing:
- @Configuration
- @EnableTransactionManagement
- @EnableJpaRepositories
- @EnableJpaAuditing
- public class PersistenceConfig { ... }
Copy
正如我们已经知道的,JPA 提供了@EntityListeners注释来指定回调侦听器类。Spring Data 提供了自己的 JPA 实体侦听器类,AuditingEntityListener。因此,让我们指定Bar实体的侦听器:
- @Entity
- @EntityListeners(AuditingEntityListener.class)
- public class Bar { ... }
Copy
现在,我们可以在持久化和更新Bar实体时捕获侦听器的审核信息。
接下来,我们将添加两个新属性,用于将创建日期和上次修改日期存储到我们的 Bar实体。属性由相应的@CreatedDate和@LastModifiedDate注释进行批注,并且其值会自动设置:
- @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;
-
- //...
-
- }
Copy
通常,我们将属性移动到基类(注释@MappedSuperClass),所有受审计实体都将扩展该基类。在我们的示例中,为了简单起见,我们将它们直接添加到Bar中。
如果我们的应用程序使用 Spring 安全性,我们可以跟踪何时进行更改以及更改者:
- @Entity
- @EntityListeners(AuditingEntityListener.class)
- public class Bar {
-
- //...
-
- @Column(name = "created_by")
- @CreatedBy
- private String createdBy;
-
- @Column(name = "modified_by")
- @LastModifiedBy
- private String modifiedBy;
-
- //...
-
- }
Copy
用@CreatedBy和@LastModifiedBy批注的列填充有创建或上次修改实体的主体的名称。该信息来自SecurityContext 的身份验证实例。如果我们想自定义设置为注释字段的值,我们可以实现AuditorAware
- public class AuditorAwareImpl implements AuditorAware<String> {
-
- @Override
- public String getCurrentAuditor() {
- // your custom logic
- }
-
- }
Copy
为了将应用程序配置为使用AuditorAwareImpl查找当前主体,我们声明了一个AuditorAware类型的 bean,使用AuditorAwareImpl 的实例进行初始化,并将 Bean 的名称指定为auditorAwareRef参数的值@EnableJpaAuditing:
- @EnableJpaAuditing(auditorAwareRef="auditorProvider")
- public class PersistenceConfig {
-
- //...
-
- @Bean
- AuditorAware<String> auditorProvider() {
- return new AuditorAwareImpl();
- }
-
- //...
-
- }
Copy
在本文中,我们研究了实现审核功能的三种方法:
本文的示例在GitHub 存储库中提供。