12 JPA 的 Auditing 功能
12.1 Auditing 是什么
Auditing 是帮我们做审计⽤的,当我们操作⼀条记录的时候,需要知道这是谁创建的、什么时间创建的、最后修改⼈是谁、最后修改时间是什么时候,甚⾄需要修改记录……这些都是 Spring Data JPA ⾥⾯的 Auditing ⽀持的,它为我们提供了四个注解来完成上⾯说的⼀系列事情,如下:
- @CreatedBy 是哪个⽤户创建的。
- @CreatedDate 创建的时间。
- @LastModifiedBy 最后修改实体的⽤户。
- @LastModifiedDate 最后⼀次修改的时间。
这就是 Auditing 了,那么它具体怎么实现呢?
12.2 如何实现 Auditing
利⽤上⾯的四个注解实现⽅法,⼀共有三种⽅式实现 Auditing,我们分别看看。
12.2.1 第一种方式:直接在实例里面添加上述四个注解
我们还⽤之前的例⼦,把 User 实体添加四个字段,分别记录创建⼈、创建时间、最后修改⼈、最后修改时间。
第⼀步:在 @Entity:User ⾥⾯添加四个注解,并且新增 @EntityListeners(AuditingEntityListener.class) 注解。
添加完之后,User 的实体代码如下:
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@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;
private boolean deleted;
@CreatedBy
private Integer createUserId;
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedBy
private Integer lastModifiedUserId;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
在 @Entity 实体中我们需要做两点操作:
- 其中最主要的四个字段分别记录创建⼈、创建时间、最后修改⼈、最后修改时间,代码如下:
@CreatedBy
private Integer createUserId;
@CreatedDate
private Date createdDate;
@LastModifiedBy
private Integer lastModifiedUserId;
@LastModifiedDate
private Date lastModifiedDate;
- 其中 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() {
return Optional.of(new Random().nextInt());
}
}
这⾥关键的⼀步,是实现 AuditorAware 接⼝的⽅法,如下所示:
public interface AuditorAware<T> {
T getCurrentAuditor();
}
第三步:通过 @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();
}
}
经验之谈:
- 这⾥说⼀个 Congifuration 的最佳实践的写法。我们为什么要单独写⼀个 JpaConfiguration 的配置⽂件,⽽不是把 @EnableJpaAuditing 放在 JpaApplication 的类⾥⾯呢?因为这样的话 JpaConfiguration ⽂件可以单独加载、单独测试,如果都放在 Appplication 类⾥⾯的话,岂不是每次测试都要启动整个应⽤吗?
- MyAuditorAware 也可以通过 @Component 注解进⾏加载,我为什么推荐 @Bean 的⽅式呢?因为这种⽅式可以让使⽤的⼈直接通过我们的配置⽂件知道我们⾃定义了哪些组件,不会让⽤的⼈产⽣不必要的惊讶,这是⼀点写 framework 的经验,供你参考。
第四步:我们写个测试⽤例测试⼀下。
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Import(JpaConfiguration.class)
class AuditTest {
@Autowired
private UserRepository userRepository;
@MockBean
private MyAuditorAware myAuditorAware;
@Test
void testAuditing() {
// 我们这⾥利⽤ @MockBean,mock 掉我们的⽅法,期待返回 13 这个⽤户 ID
Mockito.when(myAuditorAware.getCurrentAuditor())
.thenReturn(Optional.of(13));
// 我们没有显式的指定更新时间、创建时间、更新⼈、创建⼈
User user = User.builder()
.name("zzn")
.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).getLastModifiedDate());
System.out.println(users.get(0));
}
}
需要注意的是:
- 我们利⽤ @MockBean 模拟 MyAuditorAware 返回结果 13 这个 UserID;
- 我们测试并验证 create_user_id 是否是我们预期的。
测试结果如下:
User(id=1, name=jack, email=123456@126.com, sex=BOY, age=20, deleted=false, createUserId=13, createdDate=2022-07-29T22:45:12.626, lastModifiedUserId=13, lastModifiedDate=2022-07-29T22:45:12.626)
结果完全符合我们的预期。
那么现在是不是学会了 Auditing 的第⼀种⽅式呢?此外,Spring Data JPA 还给我们提供了第⼆种⽅式:实体直接实现 Auditable 接⼝即可,我们来看⼀下。
12.2.2 第二种方式:在实体里面实现 Auditable 接口
我们改⼀下上⾯的 User 实体对象,如下:
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class User implements Auditable<Integer, Long, LocalDateTime> {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String email;
@Enumerated(EnumType.STRING)
private SexEnum sex;
private Integer age;
private boolean deleted;
@CreatedBy
private Integer createUserId;
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedBy
private Integer lastModifiedUserId;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@Override
public Optional<Integer> getCreatedBy() {
return Optional.ofNullable(this.createUserId);
}
@Override
public void setCreatedBy(Integer createdBy) {
this.createUserId = createdBy;
}
@Override
public Optional<LocalDateTime> getCreatedDate() {
return Optional.ofNullable(this.createdDate);
}
@Override
public void setCreatedDate(LocalDateTime creationDate) {
this.createdDate = creationDate;
}
@Override
public Optional<Integer> getLastModifiedBy() {
return Optional.ofNullable(this.lastModifiedUserId);
}
@Override
public void setLastModifiedBy(Integer lastModifiedBy) {
this.lastModifiedUserId = lastModifiedBy;
}
@Override
public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
@Override
public Optional<LocalDateTime> getLastModifiedDate() {
return Optional.ofNullable(this.lastModifiedDate);
}
@Override
public boolean isNew() {
return id == null;
}
}
与第⼀种⽅式的差异是,这⾥我们要去掉上⾯说的四个注解,并且要实现接⼝ Auditable 的⽅法,代码会变得很冗余和啰唆。
⽽其他都不变,我们再跑⼀次刚才的测试⽤例,发现效果是⼀样的。从代码的复杂程度来看,这种⽅式我不推荐使⽤。那么我们再看⼀下第三种⽅式。
12.2.3 第三种方式:利用 @MappedSuperclass 注解
我们在第 6 课时讲对象的多态的时候提到过这个注解,它主要是⽤来解决公共 BaseEntity 的问题,⽽且其代表的是继承它的每⼀个类都是⼀个独⽴的表。
我们先看⼀下 @MappedSuperclass 的语法
@Documented
@Target({TYPE})
@Retention(RUNTIME)
public @interface MappedSuperclass {
}
它注解⾥⾯什么都没有,其实就是代表了抽象关系,即所有⼦类的公共字段⽽已。那么接下来我们看⼀下实例。
第⼀步:创建⼀个 BaseEntity,⾥⾯放⼀些实体的公共字段和注解。
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedBy
protected Integer createUserId;
@CreatedDate
protected LocalDateTime createdDate;
@LastModifiedBy
protected Integer lastModifiedUserId;
@LastModifiedDate
protected LocalDateTime lastModifiedDate;
}
注意: BaseEntity ⾥⾯需要⽤上⾯提到的四个注解,并且加上 @EntityListeners(AuditingEntityListener.class),这样所有的⼦类就不需要加了。
实际⼯作中,BaseEntity 可能还更复杂⼀点,⽐如说把 ID 和 @Version 加进去,会变成如
下形式:
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
@Version
protected Long version;
protected boolean deleted;
@CreatedBy
protected Integer createUserId;
@CreatedDate
protected LocalDateTime createdDate;
@LastModifiedBy
protected Integer lastModifiedUserId;
@LastModifiedDate
protected LocalDateTime lastModifiedDate;
}
第⼆步:实体直接继承 BaseEntity 即可。
我们修改⼀下上⾯的 User 实例继承 BaseEntity,代码如下:
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class User extends BaseEntity {
private String name;
private String email;
@Enumerated(EnumType.STRING)
private SexEnum sex;
private Integer age;
}
这样的话,User 实体就不需要关⼼太多,我们只关注⾃⼰需要的逻辑即可,如下:
- 去掉了 @EntityListeners(AuditingEntityListener.class);
- 去掉了 @CreatedBy、@CreatedDate、@LastModifiedBy、@LastModifiedDate 四个注解的公共字段。
接着我们再跑⼀下上⾯的测试⽤例,发现效果还是⼀样的。
这种⽅式,是我最推荐的,也是实际⼯作中使⽤最多的⼀种⽅式。它的好处显⽽易⻅就是公⽤性强,代码简单,需要关⼼的少。
通过上⾯的实际案例,我们其实也能很容易发现 Auditing 帮我们解决了什么问题,下⾯总结⼀下。
12.3 JPA 的 Auditing 功能解决了哪些问题
- 可以很容易地让我们写⾃⼰的 BaseEntity,把⼀些公共的字段放在⾥⾯,不需要我们关⼼太多和业务⽆关的字段,更容易让我们公司的表更加统⼀和规范,就是统⼀加上 id、version、deleted、@CreatedBy、@CreatedDate、@LastModifiedBy、@LastModifiedDate 等。
- Auditing 在实战应⽤场景中,⽐较适合做后台管理项⽬,对应纯粹的 RestAPI 项⽬,提供给⽤户直接查询的 API 的话,可以考虑⼀个特殊的 UserID
12.4 Auditing 的实现原理
⽅法你应该已经掌握了,其实这个时候我们应该好奇⼀下,其原理是怎么实现的?我们来操作⼀下。
12.4.1 源码分析
第⼀步:还是从 @EnableJpaAuditing ⼊⼿分析。
我们前⾯讲了它的使⽤⽅法,这次我们分析⼀下其加载原理,看下⾯的图:
通过包名可以知道,⾸先 Auditing 这套封装是 Spring Data JPA 实现的,⽽不是 Java Persistence API 规定的,其注解⾥⾯还有⼀项重要功能就是 @Import(JpaAuditingRegistrar.class) 这个类,它帮我们处理 Auditing 的逻辑。
我们看其源码,⼀步⼀步地 debug 下去可以发现如下所示:
进⼀步进⼊到如下⽅法中:
可以看到 Spring 容器给 AuditingEntityListener.class 注⼊了⼀个 AuditingHandler 的处理类。
第⼆步:打开 AuditingEntityListener.class 的源码分析 debug ⼀下。
@Configurable
public class AuditingEntityListener {
private @Nullable ObjectFactory<AuditingHandler> handler;
public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
this.handler = auditingHandler;
}
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markCreated(target);
}
}
}
@PreUpdate
public void touchForUpdate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markModified(target);
}
}
}
}
从源码我们可以看到,AuditingEntityListener 的实现还是⽐较简单的,利⽤了 Java Persistence API ⾥⾯的@PrePersist、@PreUpdate 回调函数,在更新和创建之前通过 AuditingHandler 添加了⽤户信息和时间信息
12.4.2 结论
- 查看 Auditing 的实现源码,其实给我们提供了⼀个思路,就是怎么利⽤ @PrePersist、@PreUpdate 等回调函数和 @EntityListeners 定义⾃⼰的框架代码。这是值得我们学习和参考的,⽐如说 Auditing 的操作⽇志场景等。
- 想成功配置 Auditing 功能,必须将 @EnableJpaAuditing 和 @EntityListeners(AuditingEntityListener.class) ⼀起使⽤才有效。
- 我们是不是可以不通过 Spring data JPA 给我们提供的 Auditing 功能,⽽是直接使⽤ @PrePersist、@PreUpdate 回调函数注解在实体上,也可以达到同样的效果呢?答案是肯定的,因为回调函数是实现的本质。
12.5 本章小结
本课时我们详细讲解了 Auditing 的使⽤⽅法,以及最佳实践是什么,还分析了 Auditing 的实现原理。