使用JPA、Hibernate和Spring Data JPA进行审计

2023/05/18

1. 概述

在ORM的上下文中,数据库审计意味着跟踪和记录与持久实体相关的事件,或者只是实体版本控制。受SQL触发器的启发,事件是对实体的插入、更新和删除操作。数据库审计的好处类似于源代码版本控制提供的好处。

在本教程中,我们将演示三种将审计引入应用程序的方法。首先,我们将使用标准JPA来实现它。接下来,我们将了解两个提供自己审计功能的JPA扩展,一个由Hibernate提供,另一个由Spring Data提供。

下面是我们将在本例中使用的示例相关实体Bar和Foo:

2. 使用JPA进行审计

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

2.1 @PrePersist、@PreUpdate和@PreRemove

在JPA实体类中,我们可以指定一个方法作为回调,我们可以在特定的实体生命周期事件期间调用该方法。由于我们对在相应的DML操作之前执行的回调感兴趣,@PrePersist、 @ PreUpdate和@PreRemove回调注解可用于我们的目的:

@Entity
public class Bar {

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

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

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

内部回调方法应始终返回void,并且不带任何参数。他们可以使用任何名称和任何访问级别,但不应该是static。

请注意JPA中的@Version注解与我们的主题并不严格相关;与审计数据相比,它更多地与乐观锁定有关。

2.2 实现回调方法

但是这种方法有一个很大的限制。正如JPA 2规范(JSR317)中所述:

通常,可移植应用程序的生命周期方法不应调用EntityManager或Query操作、访问其他实体实例或修改同一持久性上下文中的关系。生命周期回调方法可以修改调用它的实体的非关系状态。

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

@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来集中管理代码:

@EntityListeners(AuditListener.class)
@Entity
public class Bar { 
    // ...
}
public class AuditListener {

    @PrePersist
    @PreUpdate
    @PreRemove
    private void beforeAnyOperation(Object object) { 
        // ...
    }
}

3. Hibernate Envers

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

3.1 使用Envers

要设置Envers,我们需要将hibernate-envers依赖添加到我们的POM中:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-envers</artifactId>
    <version>${hibernate.version}</version>
</dependency>

然后我们在@Entity(审计整个实体)或特定的@Columns(如果我们只需要审计特定的属性)上添加@Audited注解:

@Entity
@Audited
public class Bar { 
    // ...
}

请注意,Bar与Foo之间存在一对多关系。在这种情况下,我们也需要通过在Foo上添加@Audited来审计Foo,或者在Bar中的关系属性上设置@NotAudited:

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

3.2 创建审计日志表

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

  • 将hibernate.hbm2ddl.auto设置为create、create-drop或update,以便Envers可以自动创建它们
  • 使用org.hibernate.tool.EnversSchemaGenerator以编程方式导出完整的数据库模式
  • 设置Ant任务以生成适当的DDL语句
  • 使用Maven插件从我们的映射(例如Juplo)生成数据库模式以导出Envers模式(适用于Hibernate 4及更高版本)

我们将采用第一条路线,因为它是最直接的,但请注意,使用hibernate.hbm2ddl.auto在生产中并不安全。

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

除此之外,默认情况下还会生成一个名为REVINFO的额外表。它包括两个重要字段REV和REVTSTMP,记录了每次修订的时间戳。我们可以猜到,bar_AUD.REV和foo_AUD.REV实际上是REVINFO.REV的外键。

3.3 配置Envers

我们可以像配置任何其他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);

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

3.4 访问实体历史

我们可以以类似于通过Hibernate Criteria API查询数据的方式来查询历史数据。我们可以使用AuditReader接口访问实体的审计历史记录,我们可以通过AuditReaderFactory使用打开的EntityManager或Session获取该接口:

AuditReader reader = AuditReaderFactory.get(session);

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

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

以下是我们如何查询Bar的修订版,这将导致获得所有状态下所有已审计Bar实例的列表:

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

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

然后我们可以使用AuditEntity工厂类指定约束:

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

4. Spring Data JPA

Spring Data JPA是一个框架,它通过在JPA提供程序的顶部添加一个额外的抽象层来扩展JPA。该层支持通过扩展Spring JPA Repository接口来创建JPA Repository。

出于我们的目的,我们可以扩展CrudRepository<T, ID extends Serializable>,这是通用CRUD操作的接口。一旦我们创建了Repository并将其注入到另一个组件,Spring Data将自动提供实现,我们就可以添加审计功能了。

4.1 启用JPA审计

首先,我们要通过注解配置启用审计。为此,我们在@Configuration类上添加@EnableJpaAuditing:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EnableJpaAuditing
public class PersistenceConfig {
    // ...
}

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

正如我们已经知道的,JPA提供了@EntityListeners注解来指定回调监听器类。Spring Data提供了自己的JPA实体监听器类AuditingEntityListener。因此,让我们为Bar实体指定监听器:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {
    // ...
}

现在我们可以在持久化和更新Bar实体时通过监听器捕获审计信息。

4.3 跟踪创建和最后修改日期

接下来,我们将添加两个新属性,用于将创建日期和上次修改日期存储到我们的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;

    //...
}

通常,我们会将属性移动到基类(由@MappedSuperClass标注),我们所有审计的实体都将扩展该基类。在我们的示例中,为了简单起见,我们将它们直接添加到Bar中。

4.4 使用Spring Security审计更改的作者

如果我们的应用程序使用Spring Security,我们可以跟踪更改的时间和更改者:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {

    //...

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

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

    //...
}

用@CreatedBy和@LastModifiedBy标注的列填充有创建或上次修改实体的主体的名称。该信息来自SecurityContext的Authentication实例。如果我们想要自定义设置为带注解字段的值,我们可以实现AuditorAware<T>接口:

public class AuditorAwareImpl implements AuditorAware<String> {

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

为了将应用程序配置为使用AuditorAwareImpl来查找当前主体,我们声明了一个AuditorAware类型的bean,使用AuditorAwareImpl实例初始化,并将bean的名称指定为@EnableJpaAuditing中的auditorAwareRef参数值:

@EnableJpaAuditing(auditorAwareRef="auditorProvider")
public class PersistenceConfig {

    //...

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

    //...
}

5. 总结

在本文中,我们考虑了三种实现审计功能的方法:

  • 纯JPA方法是最基本的,包括使用生命周期回调。但是,我们只能修改实体的非关系状态。这使得@PreRemove回调对我们的目的毫无用处,因为我们在该方法中所做的任何设置都将与实体一起被删除。
  • Envers是Hibernate提供的一个成熟的审计模块。它是高度可配置的,并且没有纯JPA实现的缺陷。因此,它允许我们审计删除操作,因为它记录到实体表以外的表中。
  • Spring Data JPA方法抽象了JPA回调的使用,并为审计属性提供了方便的注解。它还可以与Spring Security集成。缺点是继承了JPA方式的相同缺陷,因此无法对删除操作进行审计。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章