Spring Data JPA 之 Auditing


1 關於 Auditing 

Auditing 翻譯過來就是審計和審核,在實際的業務中,需要記錄一張表的操作時間及操作者,並方便地記錄操作日志,Spring Data JPA 為我們提供了審計的架構實現,並提供了4個注解專門實現這些功能

  1. @CreatedBy:由哪個用戶創建
  2. @CreatedDate:創建的時間
  3. @LastModifiedBy:最近一次修改由哪個用戶發起
  4. @LastModifiedDate:最近一次修改的時間

閱讀指引:

使用參見 1,2,4;

基本了解參見 3.1,3.2, 3.3;

深入了解工作原理參見 3.4

 

2 Auditing 配置

2.1 Customer 里的 Auditing 配置

2.1.1 添加4個注解 

1     @CreatedDate
2     private Date createdDate;
3     @CreatedBy
4     private Long createdByCustomerId;
5 
6     @LastModifiedDate
7     private Date lastModifiedDate;
8     @LastModifiedBy
9     private Long lastModifiedByCustomerId;

 

2.1.2 在實體上添加 @EntityListeners(value = {AuditingEntityListener.class})

最終的實體類應該是這個樣子

 1 @Entity
 2 @Data
 3 @EqualsAndHashCode(onlyExplicitlyIncluded = true)
 4 @EntityListeners(value = {AuditingEntityListener.class})
 5 public class Customer {
 6     @Id
 7     @GeneratedValue
 8     @EqualsAndHashCode.Include
 9     private Long id;
10     @EqualsAndHashCode.Include
11     private String name;
12 
13 
14     @CreatedDate
15     private Date createdDate;
16     @CreatedBy
17     private Long createdByCustomerId;
18 
19     @LastModifiedDate
20     private Date lastModifiedDate;
21     @LastModifiedBy
22     private Long lastModifiedByCustomerId;
23 
24 }

2.1.3 實現 AuditorAware 告訴 JPA 當前用戶

第一種:可以從 request 或者 session 中取 

 1     @Component
 2     public class AuditorAwareImpl implements AuditorAware<Long> {
 3         @Override
 4         public Optional<Long> getCurrentAuditor() {
 5             try {
 6                 // IllegalStateException
 7                 RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
 8                 Object userIdObject = requestAttributes.getAttribute("userId", RequestAttributes.SCOPE_SESSION);
 9                 if (userIdObject == null) {
10                     return Optional.empty();
11                 }
12                 // ClassCastException
13                 Long userId = (Long) userIdObject;
14                 return Optional.of(userId);
15             } catch (IllegalStateException | ClassCastException e) {
16                 return Optional.empty();
17             }
18         }
19     }

第二種:當集成了 Security 的時候,可以從 SecurityContextHolder 取得

 1     @Component
 2     public  class AuditorAwareSecurityImpl implements AuditorAware<Long> {
 3         @Override
 4         public Optional<Long> getCurrentAuditor() {
 5             try {
 6                 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 7                 Customer customer = (Customer) authentication.getPrincipal();
 8                 return Optional.of(customer.getId());
 9             } catch (ClassCastException e) {
10                 return Optional.empty();
11             }
12 
13         }
14     }

2.1.4 啟動類中加入 @EnableJpaAuditing 開啟 JPA 的 Auditing 功能 

當執行 created retrieve update 操作時,AuditingEntityListener 會自動更新四個字段的值

2.2 @MappedSuperClass

在實際工作中, 我們需要對實體進行改進,可以將多個字段抽取到一個抽象基類中

 1 @MappedSuperclass
 2 @EntityListeners(AuditingEntityListener.class)
 3 @NoArgsConstructor
 4 @Getter
 5 @Setter
 6 public abstract class AbstractAuditable {
 7     @Id
 8     @GeneratedValue
 9     protected Long id;
10 
11     @CreatedDate
12     protected Date createdDate;
13     @CreatedBy
14     protected Long createdByCustomerId;
15 
16     @LastModifiedDate
17     protected Date lastModifiedDate;
18     @LastModifiedBy
19     protected Long lastModifiedByCustomerId;
20 }

使用時, 直接繼承 AbstractAuditable 即可

@Entity
@Getter
@Setter
@NoArgsConstructor
public class CityAuditable extends  AbstractAuditable{
    private String name;
}

 

3 AuditingEntityListener 源碼解析

3.1 源碼

 1 @Configurable
 2 public class AuditingEntityListener {
 3 
 4     private @Nullable ObjectFactory<AuditingHandler> handler;
 5 
 6     /**
 7      * Configures the {@link AuditingHandler} to be used to set the current auditor on the domain types touched.
 8      *
 9      * @param auditingHandler must not be {@literal null}.
10      */
11     public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
12 
13         Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
14         this.handler = auditingHandler;
15     }
16 
17     /**
18      * Sets modification and creation date and auditor on the target object in case it implements {@link Auditable} on
19      * persist events.
20      *
21      * @param target
22      */
23     @PrePersist
24     public void touchForCreate(Object target) {
25 
26         Assert.notNull(target, "Entity must not be null!");
27 
28         if (handler != null) {
29 
30             AuditingHandler object = handler.getObject();
31             if (object != null) {
32                 object.markCreated(target);
33             }
34         }
35     }
36 
37     /**
38      * Sets modification and creation date and auditor on the target object in case it implements {@link Auditable} on
39      * update events.
40      *
41      * @param target
42      */
43     @PreUpdate
44     public void touchForUpdate(Object target) {
45 
46         Assert.notNull(target, "Entity must not be null!");
47 
48         if (handler != null) {
49 
50             AuditingHandler object = handler.getObject();
51             if (object != null) {
52                 object.markModified(target);
53             }
54         }
55     }
56 }

3.2 主類解析

進入 AuditingEntityListener,我們發現

  • 這是個被 @Configurable 標注的非 Spring 管理的 Bean;
  • 它使用了委托代理模式將具體工作交付給了 AuditingHandler 進行;
  • 它的具體方法 touchForCreate 上有 javax.persistence.PrePersist 注解
  • 它的具體方法 touchForUpdate 上有 javax.persistence.PreUpdate 注解

3.3 流程解析

點進主方法 touchForCreate(Object target) 方法,我們發現該方法嘗試從 ObjectFactory 獲取 AuditingHandler, 並調用 AuditingHandler#markCreated(T source)  方法標注這個類為新建;

點進 AuditingHandler#markCreated(T source) 方法我們發現該方法調用了 AuditingHandler#touch(T target, boolean isNew) 方法;

點進 AuditingHandler#touch(T target, boolean isNew) 方法,我們發現該方法嘗試使用 DefaultAuditableBeanWrapperFactory#getBeanWrapperFor(T source) 獲取一個包裝好 Auditable 類型 Bean 實體的 AuditableBeanWrapper 實例;然后依次調用 AuditingHandler#touchAuditor(AuditableBeanWrapper<?> wrapper, boolean isNew) 設置創建者,調用 AuditingHandler#touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) 設置創建時間

3.4 相關類及方法解析

3.4.1 DefaultAuditableBeanWrapperFactory#getBeanWrapperFor

如果當前實體類目標實現了 Auditable 接口 ,則直接返回一個 Auditable 的包裝類  AuditableInterfaceBeanWrapper

否則調用 AnnotationAuditingMetadata.getMetadata(target.getClass()) 獲取目標類的元數據,根據元素據判斷 target 是否是一個 Auditable (可 Auditing)實例(至少含有四個 Auditing 基本標簽中的一個),如果是則返回一個包裝類  ReflectionAuditingBeanWrapper

 1 /**
 2      * Returns an {@link AuditableBeanWrapper} if the given object is capable of being equipped with auditing information.
 3      *
 4      * @param source the auditing candidate.
 5      * @return
 6      */
 7     @SuppressWarnings("unchecked")
 8     public <T> Optional<AuditableBeanWrapper<T>> getBeanWrapperFor(T source) {
 9 
10         Assert.notNull(source, "Source must not be null!");
11 
12         return Optional.of(source).map(it -> {
13 
14             if (it instanceof Auditable) {
15                 return (AuditableBeanWrapper<T>) new AuditableInterfaceBeanWrapper((Auditable<Object, ?, TemporalAccessor>) it);
16             }
17 
18             AnnotationAuditingMetadata metadata = AnnotationAuditingMetadata.getMetadata(it.getClass());
19 
20             if (metadata.isAuditable()) {
21                 return new ReflectionAuditingBeanWrapper<T>(it);
22             }
23 
24             return null;
25         });
26     }

3.4.2 AnnotationAuditingMetadata#getMetadata(Class<?> type)

該方法 從 AnnotationAuditingMetadata類 中的 ConcurrentHashMap類型的緩存  METADATA_CACHE 中獲取或者保存一個新的 AnnotationAuditingMetadata 實例

3.4.3 AnnotationAuditingMetadata

該類的主要作用是根據 四個 Auditing 注解,構成被給予的類的元數據,並提供 isAuditable() 方法供調用者檢查被給予類是否為 Auditable(可 Auditing) 實例。

3.4.3.1 類成員1:

CREATED_BY_FILTERCREATED_DATE_FILTERLAST_MODIFIED_BY_FILTERLAST_MODIFIED_DATE_FILTER四個 Filter 字段從 Auditing 四個基本注解類構成;

1     private static final AnnotationFieldFilter CREATED_BY_FILTER = new AnnotationFieldFilter(CreatedBy.class);
2     private static final AnnotationFieldFilter CREATED_DATE_FILTER = new AnnotationFieldFilter(CreatedDate.class);
3     private static final AnnotationFieldFilter LAST_MODIFIED_BY_FILTER = new AnnotationFieldFilter(LastModifiedBy.class);
4     private static final AnnotationFieldFilter LAST_MODIFIED_DATE_FILTER = new AnnotationFieldFilter(
5             LastModifiedDate.class);

3.4.3.2 類成員2:

四個 Optional<Field> 與 四個基本注解標注的實體屬性字段相對應;

    private final Optional<Field> createdByField;
    private final Optional<Field> createdDateField;
    private final Optional<Field> lastModifiedByField;
    private final Optional<Field> lastModifiedDateField;

3.4.3.3 類成員3:

SUPPORTED_DATE_TYPES:存放支持的時間類型,供后邊檢查 createdDateField 和 lastModifiedDateField 兩個時間域聲明的 class 正確與否;

 1 static final List<String> SUPPORTED_DATE_TYPES;
 2 
 3     static {
 4 
 5         List<String> types = new ArrayList<>(5);
 6         types.add("org.joda.time.DateTime");
 7         types.add("org.joda.time.LocalDateTime");
 8         types.add(Date.class.getName());
 9         types.add(Long.class.getName());
10         types.add(long.class.getName());
11 
12         SUPPORTED_DATE_TYPES = Collections.unmodifiableList(types);
13     }

3.4.3.4 構造方法:

使用反射與四個 Filter 在目標實體類上獲取四個參與 Auditing 的 Field 域,並包裝成 Optional;

檢查兩個時間域的類型是否正確;

 1     private AnnotationAuditingMetadata(Class<?> type) {
 2 
 3         Assert.notNull(type, "Given type must not be null!");
 4 
 5         this.createdByField = Optional.ofNullable(ReflectionUtils.findField(type, CREATED_BY_FILTER));
 6         this.createdDateField = Optional.ofNullable(ReflectionUtils.findField(type, CREATED_DATE_FILTER));
 7         this.lastModifiedByField = Optional.ofNullable(ReflectionUtils.findField(type, LAST_MODIFIED_BY_FILTER));
 8         this.lastModifiedDateField = Optional.ofNullable(ReflectionUtils.findField(type, LAST_MODIFIED_DATE_FILTER));
 9 
10         assertValidDateFieldType(createdDateField);
11         assertValidDateFieldType(lastModifiedDateField);
12     }

3.4.3.5 方法 assertValidDateFieldType

該方法首先檢查成員 SUPPORTED_DATE_TYPES 中是否包含 filed 所指示的時間類型,否則檢查是否是 Jsr310(JAVA 8) 支持的時間類型,或者是 org.threeten.bp.LocalDateTime 類型;

因此該方法覆蓋的時間類型為12種,分別為:

  • org.joda.time.DateTime
  • org.joda.time.LocalDateTime
  • java.util.Date
  • java.lang.Long
  • PrimitiveType.long
  • java.time.LocalDateTime(JDK>=1.8)
  • java.time.LocalDate(JDK>=1.8)
  • java.time.LocalTime(JDK>=1.8)
  • java.time.Instant(JDK>=1.8)
  • org.threeten.bp.Instant;("org.threeten.bp.LocalDateTime" exists in classPath)
  • org.threeten.bp.LocalDate;("org.threeten.bp.LocalDateTime" exists in classPath)
  • org.threeten.bp.LocalDateTime;("org.threeten.bp.LocalDateTime" exists in classPath)
  • org.threeten.bp.LocalTime;("org.threeten.bp.LocalDateTime" exists in classPath)
 1 private void assertValidDateFieldType(Optional<Field> field) {
 2 
 3         field.ifPresent(it -> {
 4 
 5             if (SUPPORTED_DATE_TYPES.contains(it.getType().getName())) {
 6                 return;
 7             }
 8 
 9             Class<?> type = it.getType();
10 
11             if (Jsr310Converters.supports(type) || ThreeTenBackPortConverters.supports(type)) {
12                 return;
13             }
14 
15             throw new IllegalStateException(String.format(
16                     "Found created/modified date field with type %s but only %s as well as java.time types are supported!", type,
17                     SUPPORTED_DATE_TYPES));
18         });
19     }

 3.4.3.6 方法 isAuditable()

該方法只檢查存在任意一個或者以上的四個基本 Auditing 注解, 因此我們的審計可以只用到一個注解

1     public boolean isAuditable() {
2         return Optionals.isAnyPresent(createdByField, createdDateField, lastModifiedByField, lastModifiedDateField);
3     }

3.4.2 AuditingHandler#touchAuditor

該方法調用 AuditorAware#getCurrentAuditor 從 AuditorAware 的實例中獲取當前的創建/修改者 Auditor ,並設置到 包裝類包含的 實體屬性中

該方法中使用的 AuditorAware 即為我們在 2.1.3 中的實現 !!

 1     private Optional<Object> touchAuditor(AuditableBeanWrapper<?> wrapper, boolean isNew) {
 2 
 3         Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
 4 
 5         return auditorAware.map(it -> {
 6 
 7             Optional<?> auditor = it.getCurrentAuditor();
 8 
 9             Assert.notNull(auditor,
10                     () -> String.format("Auditor must not be null! Returned by: %s!", AopUtils.getTargetClass(it)));
11 
12             auditor.filter(__ -> isNew).ifPresent(foo -> wrapper.setCreatedBy(foo));
13             auditor.filter(__ -> !isNew || modifyOnCreation).ifPresent(foo -> wrapper.setLastModifiedBy(foo));
14 
15             return auditor;
16         });
17     }

3.4.2.1 方法 AuditableBeanWrapper#setCreatedBy

該方法對應 3.4.1中獲取到的包裝實例分別對應:

I. 當 target 是 Auditable 的實現,則調用:  AuditableInterfaceBeanWrapper#setCreatedBy, 直接調用 Auditable#setter 

1 @Override
2         public Object setCreatedBy(Object value) {
3        
4             auditable.setCreatedBy(value);
5             return value;
6         }

II. 當 target 是 可 Auditing(有注解),則調用:ReflectionAuditingBeanWrapper#setCreatedBy,根據包裝實例提供的元數據(見 3.4.3.2 與 該反射類的構造方法),使用反射在對應的 Field 域設置值 

 1     @Override
 2     public Object setCreatedBy(Object value) {
 3         return setField(metadata.getCreatedByField(), value);
 4     }
 5 
 6     public Optional<Field> getCreatedByField() {
 7         return createdByField;
 8     }
 9 
10 
11 
12     private <S> S setField(Optional<Field> field, S value) {
13 
14         field.ifPresent(it -> ReflectionUtils.setField(it, target, value));
15 
16         return value;
17     }

3.4.3 AuditingHandler#touchDate

該方法調用 DateTimeProvider#getNow 從 DateTimeProvider (默認為: CurrentDateTimeProvider)中獲得 TemporalAccessor 實例,並設置到 包裝類包含的 實體屬性中

 1     private Optional<TemporalAccessor> touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) {
 2 
 3         Assert.notNull(wrapper, "AuditableBeanWrapper must not be null!");
 4 
 5         Optional<TemporalAccessor> now = dateTimeProvider.getNow();
 6 
 7         Assert.notNull(now, () -> String.format("Now must not be null! Returned by: %s!", dateTimeProvider.getClass()));
 8 
 9         now.filter(__ -> isNew).ifPresent(it -> wrapper.setCreatedDate(it));
10         now.filter(__ -> !isNew || modifyOnCreation).ifPresent(it -> wrapper.setLastModifiedDate(it));
11 
12         return now;
13     }

3.4.3.1 方法 AuditableBeanWrapper#setCreatedDate

該方法對應 3.4.1中獲取到的包裝實例分別對應:

I. 當 target 是 Auditable 的實現,則調用:  AuditableInterfaceBeanWrapper#setCreatedDate, 直接調用 Auditable#setter 

1     @Override
2         public TemporalAccessor setCreatedDate(TemporalAccessor value) {
3 
4             auditable.setCreatedDate(getAsTemporalAccessor(Optional.of(value), type).orElseThrow(IllegalStateException::new));
5 
6             return value;
7         }

II. 當 target 是 可 Auditing(有注解),則調用:ReflectionAuditingBeanWrapper#setCreatedDate,根據包裝實例提供的元數據(見 3.4.3.2 與 該反射類的構造方法),使用反射在對應的 Field 域設置值 

1 @Override
2         public TemporalAccessor setCreatedDate(TemporalAccessor value) {
3 
4             return setDateField(metadata.getCreatedDateField(), value);
5         }

 

3.4.3.2 TemporalAccessor

I. 當 EnableJpaAuditing#dateTimeProviderRef 未配置時, 默認使用 LocalDateTime

    protected BeanDefinitionBuilder configureDefaultAuditHandlerAttributes(AuditingConfiguration configuration,
            BeanDefinitionBuilder builder) {

        if (StringUtils.hasText(configuration.getAuditorAwareRef())) { builder.addPropertyValue(AUDITOR_AWARE, createLazyInitTargetSourceBeanDefinition(configuration.getAuditorAwareRef())); } else { builder.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE); } builder.addPropertyValue(SET_DATES, configuration.isSetDates()); builder.addPropertyValue(MODIFY_ON_CREATE, configuration.isModifyOnCreate()); if (StringUtils.hasText(configuration.getDateTimeProviderRef())) { builder.addPropertyReference(DATE_TIME_PROVIDER, configuration.getDateTimeProviderRef());// 使用已配置的 provider } else { builder.addPropertyValue(DATE_TIME_PROVIDER, CurrentDateTimeProvider.INSTANCE);//使用默認的 LocalDateTime } builder.setRole(AbstractBeanDefinition.ROLE_INFRASTRUCTURE); return builder; }

II. 當 EnableJpaAuditing#dateTimeProviderRef 已配置時,使用配置的 Provider (見3.4.3.5 中的種時間類型,它們都實現了該接口),如何實現該 Provider 參考 CurrentDateTimeProvider

 

4 Listener 事件擴展

4.1 Java Persistence API 

通過對源碼的分析與調試,我們可以自定義自己的 Listener,需要注意的是 Java Persistence API 底層又為我們提供了如下的 Callbacks 注解

 
Type Description 簡要描述
@PrePersist

Excecuted before the entity manager persist operation is actually executed or cascaded.
This call is synchronous with the persist operation.

新增之前
@PreRemove

Excecuted before the entity manager remove operation is actually executed or cascaded.
This call is synchronous with the remove operation.

刪除之前
@PostPersist

Excecuted after the entity manager persist operation is actually executed or cascaded.
This call is invoked after the database INSERT is executed.

新增之后
@PostRemove

Excecuted after the entity manager remove operation is actually executed or cascaded.
This call is synchronous with the remove operation.

刪除之后
@PreUpdate Invoked before the database UPDATE is executed. 更新之前
@PostUpdate Invoked after the database UPDATE is executed. 更新之后
@PostLoad Invoked after an entity has been loaded into the current persistence context or an entity has been refreshed. 加載之后

 

4.2 自定義 EntityListener

1. 新建一個 EntityListener ActionsLogsListener

 1 @CommonsLog
 2 public class ActionsLogsListener<E extends Serializable> {
 3     @PostPersist
 4     public void postPersist(E entity) {
 5         this.notice(entity, NoticeType.create);
 6     }
 7 
 8     @PostRemove
 9     public void postRemove(E entity) {
10         this.notice(entity, NoticeType.remove);
11     }
12 
13     @PostUpdate
14     public void postUpdate(E entity) {
15         this.notice(entity, NoticeType.update);
16     }
17 
18     @PostLoad
19     public void postLoad(E entity) {
20         this.notice(entity, NoticeType.load);
21     }
22 
23     private void notice(E e, NoticeType type) {
24         Preconditions.checkArgument(e != null && type != null);
25         log.info(String.format("%s 執行了 %s 操作", e, type.getDescription()));
26     }
27 
28     @Getter
29     @RequiredArgsConstructor
30     private enum NoticeType {
31         create("創建"),
32         remove("刪除"),
33         update("更新"),
34         load("查詢");
35         private final String description;
36     }
37 }

 

 

2. 添加該 CustomActLogsListener 到 EntityListeners#value 中

 

 1 @Entity
 2 @Data
 3 @EqualsAndHashCode(onlyExplicitlyIncluded = true)
 4 @EntityListeners(value = {AuditingEntityListener.class,ActionsLogsListener.class})
 5 public class Customer {
 6     @Id
 7     @GeneratedValue
 8     @EqualsAndHashCode.Include
 9     private Long id;
10     @EqualsAndHashCode.Include
11     private String name;
12 
13 
14     @CreatedDate
15     private Date createdDate;
16     @CreatedBy
17     private Long createdByCustomerId;
18 
19     @LastModifiedDate
20     private Date lastModifiedDate;
21     @LastModifiedBy
22     private Long lastModifiedByCustomerId;
23 
24 }

5 附

5.1 判斷當前使用的 JDK>=1.8?

public static final boolean IS_JDK_8 = org.springframework.util.ClassUtils.isPresent("java.time.Clock",
            AnnotationAuditingMetadata.class.getClassLoader());

5.2 使用枚舉類型的單例

public interface DateTimeProvider {

    /**
     * Returns the current time to be used as modification or creation date.
     *
     * @return
     */
    Optional<TemporalAccessor> getNow();
}

public enum CurrentDateTimeProvider implements DateTimeProvider {

    INSTANCE;

    /*
     * (non-Javadoc)
     * @see org.springframework.data.auditing.DateTimeProvider#getNow()
     */
    @Override
    public Optional<TemporalAccessor> getNow() {
        return Optional.of(LocalDateTime.now());
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM