JPA 的审计功能

简介: 简单介绍下JPA框架实现的审计功能

Auditing 指的是什么?

Auditing 是帮我们做审计用的,当我们操作一条记录的时候,需要知道这是谁创建的、什么时间创建的、最后修改人是谁、最后修改时间是什么时候,甚至需要修改记录……这些都是 Spring Data JPA 里面的 Auditing 支持的,它为我们提供了四个注解来完成上面说的一系列事情,如下:

  • @CreatedBy 是哪个用户创建的。
  • @CreatedDate 创建的时间。
  • @LastModifiedBy 最后修改实体的用户。
  • @LastModifiedDate 最后一次修改的时间。

这就是 Auditing 了,那么它具体怎么实现呢?

Auditing 如何实现?

利用上面的四个注解实现方法,一共有三种方式实现 Auditing,我们分别看看。

第一种方式:直接在实例里面添加上述四个注解

我们还用之前的例子,把 User 实体添加四个字段,分别记录创建人、创建时间、最后修改人、最后修改时间。

第一步:在 @Entity:User 里面添加四个注解,并且新增 @EntityListeners(AuditingEntityListener.class) 注解。

添加完之后,User 的实体代码如下:

@Entity

@Data

@Builder

@AllArgsConstructor

@NoArgsConstructor

@ToString(exclude = "addresses")

@EntityListeners(AuditingEntityListener.class)

public class User implements Serializable {

   @Id

   @GeneratedValue(strategy= GenerationType.AUTO)

   private Long id;

   private String name;

   private String email;

   @Enumerated(EnumType.STRING)

   private SexEnum sex;

   private Integer age;

   @OneToMany(mappedBy = "user")

   @JsonIgnore

   private List<UserAddress> addresses;

   private Boolean deleted;

   @CreatedBy

   private Integer createUserId;

   @CreatedDate

   private Date createTime;

   @LastModifiedBy

   private Integer lastModifiedUserId;

   @LastModifiedDate

   private Date lastModifiedTime;

}

在 @Entity 实体中我们需要做两点操作:

1.其中最主要的四个字段分别记录创建人、创建时间、最后修改人、最后修改时间,代码如下:

   @CreatedBy

   private Integer createUserId;

   @CreatedDate

   private Date createTime;

   @LastModifiedBy

   private Integer lastModifiedUserId;

   @LastModifiedDate

   private Date lastModifiedTime;

2.其中 AuditingEntityListener 不能少,必须通过这段代码:

@EntityListeners(AuditingEntityListener.class)

在 Entity 的实体上面进行注解。

第二步:实现 AuditorAware 接口,告诉 JPA 当前的用户是谁。

我们需要实现 AuditorAware 接口,以及 getCurrentAuditor 方法,并返回一个 Integer 的 user ID。

public class MyAuditorAware implements AuditorAware<Integer> {

   //需要实现AuditorAware接口,返回当前的用户ID

   @Override

   public Optional<Integer> getCurrentAuditor() {

      ServletRequestAttributes servletRequestAttributes =

            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

      Integer userId = (Integer) servletRequestAttributes.getRequest().getSession().getAttribute("userId");

      return Optional.ofNullable(userId);

   }

}

这里关键的一步,是实现 AuditorAware 接口的方法,如下所示:

public interface AuditorAware<T> {

   T getCurrentAuditor();

}

需要注意的是:这里获得用户 ID 的方法不止这一种,实际工作中,我们可能将当前的 user 信息放在 Session 中,可能把当前信息放在 Redis 中,也可能放在 Spring 的 security 里面管理。此外,这里的实现会有略微差异,我们以 security 为例:

Authentication authentication =  SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || !authentication.isAuthenticated()) {

  return null;

}

Integer userId = ((LoginUserInfo) authentication.getPrincipal()).getUser().getId();

这时获取 userId 的代码可能会变成上面这样子,你了解一下就好。

第三步:通过 @EnableJpaAuditing 注解开启 JPA 的 Auditing 功能。

第三步是最重要的一步,如果想使上面的配置生效,我们需要开启 JPA 的 Auditing 功能(默认没开启)。这里需要用到的注解是 @EnableJpaAuditing,代码如下:

@Inherited

@Documented

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Import(JpaAuditingRegistrar.class)

public @interface EnableJpaAuditing {

//auditor用户的获取方法,默认是找AuditorAware的实现类;

String auditorAwareRef() default "";

//是否在创建修改的时候设置时间,默认是true

boolean setDates() default true;

//在创建的时候是否同时作为修改,默认是true

boolean modifyOnCreate() default true;

//时间的生成方法,默认是取当前时间(为什么提供这个功能呢?因为测试的时候有可能希望时间保持不变,它提供了一种自定义的方法);

String dateTimeProviderRef() default "";

}

在了解了@EnableJpaAuditing注解之后,我们需要创建一个Configuration 文件,添加 @EnableJpaAuditing 注解,并且把我们的 MyAuditorAware 加载进去即可,如下所示:

@Configuration

@EnableJpaAuditing

public class JpaConfiguration {

   @Bean

   @ConditionalOnMissingBean(name = "myAuditorAware")

   MyAuditorAware myAuditorAware() {

      return new MyAuditorAware();

   }

}

经验之谈:

  1. 这里说一个 Congifuration 的最佳实践的写法。我们为什么要单独写一个JpaConfiguration的配置文件,而不是把@EnableJpaAuditing 放在 JpaApplication 的类里面呢?因为这样的话 JpaConfiguration 文件可以单独加载、单独测试,如果都放在 Appplication 类里面的话,岂不是每次测试都要启动整个应用吗?
  2. MyAuditorAware 也可以通过 @Component 注解进行加载,我为什么推荐 @Bean 的方式呢?因为这种方式可以让使用的人直接通过我们的配置文件知道我们自定义了哪些组件,不会让用的人产生不必要的惊讶,这是一点写 framework 的经验,供你参考。

第四步:我们写个测试用例测试一下。

@DataJpaTest

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

@Import(JpaConfiguration.class)

public class UserRepositoryTest {

    @Autowired

    private UserRepository userRepository;

    @MockBean

    MyAuditorAware myAuditorAware;

    @Test

    public void testAuditing() {

        //由于测试用例模拟web context环境不是我们的重点,我们这里利用@MockBean,mock掉我们的方法,期待返回13这个用户ID

        Mockito.when(myAuditorAware.getCurrentAuditor()).thenReturn(Optional.of(13));

        //我们没有显式的指定更新时间、创建时间、更新人、创建人

        User user = User.builder()

                .name("jack")

                .email("123456@126.com")

                .sex(SexEnum.BOY)

                .age(20)

                .build();

        userRepository.save(user);

        //验证是否有创建时间、更新时间,UserID是否正确;

        List<User> users = userRepository.findAll();

        Assertions.assertEquals(13,users.get(0).getCreateUserId());

        Assertions.assertNotNull(users.get(0).getLastModifiedTime());

        System.out.println(users.get(0));

    }

}

需要注意的是:

  1. 我们利用 @MockBean 模拟 MyAuditorAware 返回结果 13 这个 UserID;
  2. 我们测试并验证 create_user_id 是否是我们预期的。

测试结果如下:

User(id=1, name=jack, email=123456@126.com, sex=BOY, age=20, deleted=null, createUserId=13, createTime=Sat Oct 03 21:19:57 CST 2020, lastModifiedUserId=13, lastModifiedTime=Sat Oct 03 21:19:57 CST 2020)

结果完全符合我们的预期。

那么现在是不是学会了 Auditing 的第一种方式呢?此外,Spring Data JPA 还给我们提供了第二种方式:实体直接实现 Auditable 接口即可,我们来看一下。

第二种方式:实体里面实现Auditable 接口

我们改一下上面的 User 实体对象,如下:

@Entity

@Data

@Builder

@AllArgsConstructor

@NoArgsConstructor

@ToString(exclude = "addresses")

@EntityListeners(AuditingEntityListener.class)

public class User implements Auditable<Integer,Long, Instant> {

   @Id

   @GeneratedValue(strategy= GenerationType.AUTO)

   private Long id;

   private String name;

   private String email;

   @Enumerated(EnumType.STRING)

   private SexEnum sex;

   private Integer age;

   @OneToMany(mappedBy = "user")

   @JsonIgnore

   private List<UserAddress> addresses;

   private Boolean deleted;

   private Integer createUserId;

   private Instant createTime;

   private Integer lastModifiedUserId;

   private Instant lastModifiedTime;

   @Override

   public Optional<Integer> getCreatedBy() {

      return Optional.ofNullable(this.createUserId);

   }

   @Override

   public void setCreatedBy(Integer createdBy) {

      this.createUserId = createdBy;

   }

   @Override

   public Optional<Instant> getCreatedDate() {

      return Optional.ofNullable(this.createTime);

   }

   @Override

   public void setCreatedDate(Instant creationDate) {

      this.createTime = creationDate;

   }

   @Override

   public Optional<Integer> getLastModifiedBy() {

      return Optional.ofNullable(this.lastModifiedUserId);

   }

   @Override

   public void setLastModifiedBy(Integer lastModifiedBy) {

      this.lastModifiedUserId = lastModifiedBy;

   }

   @Override

   public void setLastModifiedDate(Instant lastModifiedDate) {

      this.lastModifiedTime = lastModifiedDate;

   }

   @Override

   public Optional<Instant> getLastModifiedDate() {

      return Optional.ofNullable(this.lastModifiedTime);

   }

   @Override

   public boolean isNew() {

      return id==null;

   }

}

与第一种方式的差异是,这里我们要去掉上面说的四个注解,并且要实现接口 Auditable 的方法,代码会变得很冗余和啰唆。

而其他都不变,我们再跑一次刚才的测试用例,发现效果是一样的。从代码的复杂程度来看,这种方式我不推荐使用。那么我们再看一下第三种方式。

第三种方式:利用 @MappedSuperclass 注解

我们在第 6 课时讲对象的多态的时候提到过这个注解,它主要是用来解决公共 BaseEntity 的问题,而且其代表的是继承它的每一个类都是一个独立的表。

我们先看一下 @MappedSuperclass 的语法。

image.png

它注解里面什么都没有,其实就是代表了抽象关系,即所有子类的公共字段而已。那么接下来我们看一下实例。

第一步:创建一个 BaseEntity,里面放一些实体的公共字段和注解。

package com.example.jpa.example1.base;

import org.springframework.data.annotation.*;

import javax.persistence.MappedSuperclass;

import java.time.Instant;

@Data

@MappedSuperclass

@EntityListeners(AuditingEntityListener.class)

public class BaseEntity {

   @CreatedBy

   private Integer createUserId;

   @CreatedDate

   private Instant createTime;

   @LastModifiedBy

   private Integer lastModifiedUserId;

   @LastModifiedDate

   private Instant lastModifiedTime;

}

注意:  BaseEntity里面需要用上面提到的四个注解,并且加上@EntityListeners(AuditingEntityListener.class),这样所有的子类就不需要加了。

第二步:实体直接继承 BaseEntity 即可。

我们修改一下上面的 User 实例继承 BaseEntity,代码如下:

@Entity

@Data

@Builder

@AllArgsConstructor

@NoArgsConstructor

@ToString(exclude = "addresses")

public class User extends BaseEntity {

   @Id

   @GeneratedValue(strategy= GenerationType.AUTO)

   private Long id;

   private String name;

   private String email;

   @Enumerated(EnumType.STRING)

   private SexEnum sex;

   private Integer age;

   @OneToMany(mappedBy = "user")

   @JsonIgnore

   private List<UserAddress> addresses;

   private Boolean deleted;

}

这样的话,User 实体就不需要关心太多,我们只关注自己需要的逻辑即可,如下:

  1. 去掉了 @EntityListeners(AuditingEntityListener.class);
  2. 去掉了 @CreatedBy、@CreatedDate、@LastModifiedBy、@LastModifiedDate 四个注解的公共字段。

接着我们再跑一下上面的测试用例,发现效果还是一样的。

这种方式,是我最推荐的,也是实际工作中使用最多的一种方式。它的好处显而易见就是公用性强,代码简单,需要关心的少。

相关文章
|
5月前
|
存储 安全 数据挖掘
|
消息中间件 自然语言处理 分布式计算
阿里云大数据操作审计
这里有最全,最新的阿里云大数据产品审计汇总,文档不断更新中。
|
存储 SQL 安全
Oasys 系统审计
Oasys 系统审计
|
SQL 缓存 前端开发
记一次对wuzhicms的审计(二)
记一次对wuzhicms的审计
113 0
记一次对wuzhicms的审计(二)
|
SQL 前端开发 搜索推荐
记一次对wuzhicms的审计(一)
记一次对wuzhicms的审计
163 0
记一次对wuzhicms的审计(一)
|
存储 SQL 弹性计算
如何创建操作审计的跟踪?
客户系统上云好比把线下的“生产车间”搬到了云上,操作审计就好像这个生产车间的“监控摄像”,记录这个生产车间的所有操作记录,但我们默认只为您保留 90 天,如果您希望更长久的保存这些历史记录,那就快来是我们提供的跟踪功能吧。本文将向您介绍如何创建跟踪及合理的配置跟踪选项。
如何创建操作审计的跟踪?
|
SQL 缓存 监控
RDS审计中心--数据库安全监控利器
依托阿里云SLS日志服务及阿里云RDS SQL审计,以极低的代价采集对SQL执行的所有操作,RDS审计中心提供丰富的内置告警规则,覆盖绝大部分场景的安全、运营、性能报表,只需要做最少的配置即可实现对数据库的全方位监控。
1021 0
|
存储 监控 对象存储
操作审计最佳实践-跨账号日志收集
此文档为您介绍,当您有多个阿里云账号需要统一审计时,如何将多个账号的操作日志收集到一个账号的对象存储(OSS)或日志服务(SLS)中。
操作审计最佳实践-跨账号日志收集
|
弹性计算 监控 安全
云上资源操作审计和配置审计
本方案是面向云上资源的操作审计和配置审计,提供的最佳实践。适用于企业型客户。通过最佳实践帮助客户在本场景下更好的使用阿里云,涉及到配置审计、操作审计、函数计算、SLS、OSS等服务的实践操作。
云上资源操作审计和配置审计