基于Spring Boot Data JPA的通用audit log日志记录的设计和实现

本文介绍了如何在SpringBootDataJPA中设计一个通用的日志记录模块,包括使用EntityListener监听实体变化,通过注解定义记录规则,以及处理一对多关系的实体变化。详细阐述了各个注解的用途,如@AuditKey、@AuditColumn等,并展示了监听器的实现逻辑,如@PostPersist和@PostUpdate方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于Spring Boot Data JPA的通用audit log日志记录的设计和实现

本文会讲解关于在Spring Boot Data JPA中如何设计一个通用的日志记录模块。本文重点是设计的思路和部分的具体实现,并不会提供完整的实现代码。博文的主要目的是为了记录自己的实现思路还有给其他有相同需求的小伙伴一些想法。

既然是基于JPA的,那我们必然考虑到会使用到JPA的entity监听器,也就是EntityListener了。而我们主要的实现也会放在EntityListener中了。

需求概要

首先在设计之前,我先来明确一下设计的需求。首先就是能够记录实体上一些字段的改变,并且可以自定义这些字段在页面的显示,比如说这个字段是’status’,可能在页面上我们需要显示成’Status’, 所以我们需要可以自定义那些字段需要记录,并且可以设置最终展示的字段名是什么。

并且我们的日志记录是会通过消息的方式,统一发送到日志服务,日志服务中使用mongoDB进行存储。然后我们前端会有一个通用的日志组件,来获取对应的所有日志。所以我们需要有key和type来标记一类的数据。这个怎么理解呢?

首先type就是什么类型的数据,其实就是entity是什么,比如我们有一个student的实体,那么我们的type可能就是student。那么key是什么呢,key就是具体哪个student的数据我们需要查看。因为student中很多条记录都可能发生变化,但我们可能只想看到某个学生,所以我们还需要一个key来定位哪个学生。

还有一些需求就是,比如我在页面上点击了一个按钮之后才会触发实体的变化,那么我现在该记录中添加一些remark,比如triggerPoint是click save button之类的。还有显示的时候可以添加上一些businessKey,这个怎么理解呢?就比如我修改了一个学生的电话号码。那我点开这个学生的历史记录中就会显示如下:

update KEVIN
phone: xxxxx -> yyyyyyy

那么其中KEVIN 就是一个businessKey,主要是展示给用户看得懂的一些字段。在这里businessKey就是学生的名字。这种需求在一对多的实体中显得尤为重要,一旦我们需要看到多的一方的实体变化,我们就需要使用businessKey让用户能确定是具体哪一条记录发送了改变了。

因为产生了很多比较奇怪但又必要的需求,所以这个日志模块的设计比较多功能,也比较复杂。小伙伴可以挑着去看每个功能的具体实现,因为一些需求所以会产生出一些你认为不必要的设计,所以仅仅只是作为参考,谢谢。

注解设计

根据以上的一些信息,我们可以设计出几个注解:

  1. AuditKey
    用来标记key是什么,默认是实体的Id,当然可以配置成其他字段,而且还支持SpEL表达式。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AuditKey {

  /**
   * Spring Expression Language (SpEL) attribute.
   */
  String expression() default "";
}

比如如下,我们就不使用默认的id作为key,而是使用code的值作为key。

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "xxxx")
@AuditTable(AuditLogDataTypeEnum.XXX) //标记该实体需要监听变化
public class XXXX extends JpaAuditableAuditLog {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  
  @AuditKey
  private String code;
}
  1. AuditColumn
    用于记录哪些字段需要记录变化的,并且on这个值是用来标识该字段发生添加,修改,删除的某个时机才需要记录,不在该列表中的操作都不进行记录。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AuditColumn {

  String value();

  AuditActionEnum[] on() default {AuditActionEnum.ADD, AuditActionEnum.DELETE, AuditActionEnum.UPDATE};
}

使用如下:

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "xxxx")
@AuditTable(AuditLogDataTypeEnum.XXX) //标记该实体需要监听变化
public class XXXX extends JpaAuditableAuditLog {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  
  @AuditKey
  @AuditColumn("Code")
  private String code;
}
  1. AuditTable
    用于标识哪个实体需要监听字段变化,只有加上@AuditTable的实体才会监听变化,然后其中的value值跟上面提到的type 是一个概念,标记是什么实体,比如student
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AuditTable {

  AuditLogDataTypeEnum value();
}

使用如下:

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "xxxx")
@AuditTable(AuditLogDataTypeEnum.XXX) //标记该实体需要监听变化
public class XXXX extends JpaAuditableAuditLog {
//...
}
  1. AuditBusinessKey
    用来标记实体中哪个是具体想展示给用户的BusinessKey (上文有讲解到)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AuditBusinessKey {

}

使用如下:

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "xxxx")
@AuditTable(AuditLogDataTypeEnum.XXX) //标记该实体需要监听变化
public class XXXX extends JpaAuditableAuditLog {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  
  @AuditKey
  @AuditColumn("Code")
  @AuditBusinessKey //标注该字段的值作为BusinessKey
  private String code;
}
  1. RelatedAuditColumn
    针对于一对多的实体,用来标注多的一方。group的值主要用来分组。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RelatedAuditColumn {

  AuditLogDataTypeEnum type();

  AuditLogDataTypeEnum group() default AuditLogDataTypeEnum.ANY;
}

使用如下:

  @OneToMany
  @Builder.Default
  @RelatedAuditColumn(type = AuditLogDataTypeEnum.XXX)
  private List<XXXX> xxx = new ArrayList<>();
  1. AuditLog
    用于标记在controller层的一些方法,用来实现上文中记录remark的功能,其中remarks的值,是可以设置多个remark
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AuditLog {

  AuditRemark[] remarks() default {};
}
  1. AuditRemark
    用于配置remark的信息了,其中column是针对实体的那个字段设置remark,然后其他都是一些remark的信息,并且支持SpEL表达式,然后condition是在什么条件下这个remark才会生效
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AuditRemark {

  AuditLogDataTypeEnum type() default AuditLogDataTypeEnum.ANY;

  String column() default "[any]";

  @AliasFor(annotation = AuditRemark.class, attribute = "value")
  String triggerPoint() default "";

  String remark() default "";

  @AliasFor(annotation = AuditRemark.class, attribute = "triggerPoint")
  String value() default "";

  /**
   * Spring Expression Language (SpEL) attribute.
   */
  String triggerPointExpression() default "";

  /**
   * Spring Expression Language (SpEL) attribute.
   */
  String remarkExpression() default "";

  /**
   * Spring Expression Language (SpEL) attribute.
   * <p>The default is {@code ""}, meaning the event is always handled.
   */
  String condition() default "";

  /**
   * Spring Expression Language (SpEL) attribute.
   */
  String triggerByExpression() default "";

  String triggerBy() default "";
}

比如我们可以如下使用,表示为student实体的status中加上remark,如果不指定column,则会给所有变化的字段都加上remark

  @PutMapping("/student")
  @AuditLog(remarks = {@AuditRemark(type = AuditLogDataTypeEnum.STUDENT, column = "status", triggerPoint = "Create Button")})
  public ResponseEntity<?> createStudent(
      @RequestBody StudentDTO studentDTO) {
    return ResponseEntity.ok(.....);
  }

为了实现以上的一些需求,我们设计了这么多的注解,接下来我们来看看具体的监听器中是如何实现的。

EntityListener的具体实现

代码有一点多,我们先简单来分析一下。entityListener主要有两个方法我们需要主要关注的,一个就是@PostPersist方法,会在实体添加的时候触发,还有一个就是@PostUpdate方法。会在实体修改的时候触发。

接下来我们就是来处理实体的哪些字段有变化了。

  1. 首先我们只需要关注有带有@AuditTable的实体,没有带@AuditTable的实体,我们就不需要监听了。
  2. 我们只需要关注带@AuditColumn的字段还有带@RelatedAuditColumn的字段,这两个我们需要分开处理。因为带@RelatedAuditColumn的字段是多的一对多或者是一对一的另一方(需要分开实现),所以我们可能需要递归遍历这个实体里面的所有带@AuditColumn的字段
  3. 如何监测字段发生改变了呢?对于@PostPersist来说,新增的我们都记录,因为对于新增的来说,@AuditColumn的值都是从无变有,当然如果新的值还是null,那我们也无需关注。而对于@PostUpdate来说,我们需要获取修改前和修改后的实体,然后遍历比对每个带@AuditColumn的字段的值,如果有变化,就需要记录。所以这里需要createEntityManager 获取一个新的entityManager来获取修改前的数据,因为当前的entityManager获取到的已经是变化后的数据了。

大概的思路就是这样了,只是说具体的一些字段是list或者是一对一,一对多我们需要单独处理和递归遍历,然后最后也是按照上面的思路去监测字段变化。

@Configurable
@Slf4j
public class JpaAuditableLogListener {

  private static final String ENTITY_ID = "id";
  private final ExpressionParser parser = new SpelExpressionParser();

  @PostPersist
  public void handlePostPersist(JpaAuditableAuditLog jpaAuditable) { //处理添加的实体
    try {
      if (!jpaAuditable.getClass().isAnnotationPresent(AuditTable.class)) {
        return;
      }
      handlePersist(jpaAuditable);
      publishEvent(jpaAuditable); //最后把生成的数据发送给日志模块
    } catch (Exception e) {
      log.error("catch exception in JpaAuditableLogListener.handlePrePersist() method, exception detail:[{}]",
          ExceptionUtils.getStackTrace(e));
    } finally {
      jpaAuditable.getAuditLogDataDTOList().clear();
    }
  }

  @PostUpdate
  public void handlePostUpdate(JpaAuditableAuditLog jpaAuditable) { //处理修改后的实体
    EntityManager entityManager = null;
    try {
      if (!jpaAuditable.getClass().isAnnotationPresent(AuditTable.class)) {
        return;
      }
      ApplicationContext applicationContext = SpringApplicationContextUtil.getApplicationContext();
      if (Objects.isNull(applicationContext)) {
        return;
      }
      entityManager = applicationContext.getBean(EntityManagerFactory.class).createEntityManager();
      JpaAuditableAuditLog originalEntity = entityManager.find(jpaAuditable.getClass(), getValueFromFieldName(jpaAuditable, ENTITY_ID)); //获取修改前的实体
      handleUpdate(jpaAuditable, originalEntity);
      publishEvent(jpaAuditable);
    } catch (Exception e) {
      log.error("catch exception in JpaAuditableLogListener.handlePrePreUpdate() method, exception detail:[{}]",
          ExceptionUtils.getStackTrace(e));
    } finally {
      jpaAuditable.getAuditLogDataDTOList().clear();
      if (Objects.nonNull(entityManager)) {
        entityManager.close();
      }
    }
  }

  private void handlePersist(JpaAuditableAuditLog rootEntity) { 
    List<Field> fieldList = getFieldsWithAnnotation(rootEntity, AuditColumn.class);
    rootEntity.getAuditLogDataDTOList().addAll(fieldList.stream().filter(field ->
        Arrays.asList(field.getAnnotation(AuditColumn.class).on()).contains(AuditActionEnum.ADD)
    ).map(field -> {
      AuditLogDataTypeEnum type = rootEntity.getClass().getAnnotation(AuditTable.class).value();
      Object toValue = getValueFromFieldName(rootEntity, field.getName());
      return getAuditLogDataDTO(rootEntity, type, type, field, null, toValue, AuditActionEnum.ADD);
    }).collect(Collectors.toList()));
    handleRelateAuditColumn(rootEntity, rootEntity, null); //单独处理RelateAuditColumn的case
  }


  private void handleUpdate(JpaAuditableAuditLog rootEntity, JpaAuditableAuditLog originalEntity) {
    if (Objects.isNull(originalEntity)) {
      return;
    }
    AuditLogDataTypeEnum type = rootEntity.getClass().getAnnotation(AuditTable.class).value();
    List<Field> fieldList = getFieldsWithAnnotation(rootEntity, AuditColumn.class);
    rootEntity.getAuditLogDataDTOList().addAll(fieldList.stream().filter(field ->
        Arrays.asList(field.getAnnotation(AuditColumn.class).on()).contains(AuditActionEnum.UPDATE)
    ).map(field -> {
      Object fromValue = getValueFromFieldName(originalEntity, field.getName());
      Object toValue = getValueFromFieldName(rootEntity, field.getName());
      return getAuditLogDataDTO(rootEntity, type, type, field, fromValue, toValue, AuditActionEnum.UPDATE);
    }).collect(Collectors.toList()));
    handleRelateAuditColumn(rootEntity, rootEntity, originalEntity);//单独处理RelateAuditColumn的case
  }


  public void handleRelateAuditColumn(JpaAuditableAuditLog rootEntity, Object currentEntity, Object originalEntity) {
    List<Field> relatedAuditColumnFieldList = getFieldsWithAnnotation(currentEntity, RelatedAuditColumn.class);
    List<Field> originalEntityRelatedAuditColumnFieldList = getFieldsWithAnnotation(originalEntity, RelatedAuditColumn.class);
    if (CollectionUtils.isEmpty(relatedAuditColumnFieldList) && CollectionUtils.isEmpty(originalEntityRelatedAuditColumnFieldList)) {
      return;
    }
    for (Field relatedAuditColumnField : (CollectionUtils.isEmpty(relatedAuditColumnFieldList) ? originalEntityRelatedAuditColumnFieldList
        : relatedAuditColumnFieldList)) {
      boolean isList = Objects.requireNonNull(ResolvableType.forField(relatedAuditColumnField).resolve()).isAssignableFrom(List.class);
      if (isList) { //需要区分是list还是非list的情况
        handleRelatedEntities(rootEntity, currentEntity, originalEntity, relatedAuditColumnField); //需要递归遍历处理
      } else {
        handleRelatedEntity(rootEntity, currentEntity, originalEntity, relatedAuditColumnField);
      }
    }
  }

  private void handleRelatedEntity(JpaAuditableAuditLog rootEntity, Object currentEntity, Object originalEntity, Field relatedAuditColumnField) {
    Object originalRelateEntity = getValueFromFieldName(originalEntity, relatedAuditColumnField.getName());
    Object currentRelateEntity = getValueFromFieldName(currentEntity, relatedAuditColumnField.getName());
    this.handleRelateAuditColumn(rootEntity, currentRelateEntity, originalRelateEntity);
    handleRelatedSingleEntity(rootEntity, relatedAuditColumnField, currentRelateEntity, originalRelateEntity);
  }

  private void handleRelatedEntities(JpaAuditableAuditLog rootEntity, Object currentEntity, Object originalEntity, Field relatedAuditColumnField) {
    List<?> originalRelatedEntityList = Optional.ofNullable((List<?>) getValueFromFieldName(originalEntity, relatedAuditColumnField.getName()))
        .orElse(new ArrayList<>());
    List<?> relatedEntityList = Optional.ofNullable((List<?>) getValueFromFieldName(currentEntity, relatedAuditColumnField.getName()))
        .orElse(new ArrayList<>());
    if (CollectionUtils.isEmpty(relatedEntityList) && CollectionUtils.isEmpty(originalRelatedEntityList)) {
      return;
    }
    //对于一对多的情况(为list的情况),我们需要分别处理list中新增,list中修改,还有list中删除的case
    handleAddCase(rootEntity, relatedAuditColumnField, relatedEntityList, originalRelatedEntityList);
    handleDeleteCase(rootEntity, relatedAuditColumnField, relatedEntityList, originalRelatedEntityList);
    handleUpdateCase(rootEntity, relatedAuditColumnField, relatedEntityList, originalRelatedEntityList);
  }


  private void handleDeleteCase(JpaAuditableAuditLog rootEntity, Field relatedAuditColumnField, List<?> relatedEntityList,
      List<?> originalRelatedEntityList) {
    List<?> deleteList = originalRelatedEntityList.stream().filter(entity ->
        !relatedEntityList.stream().map(item -> getValueFromFieldName(item, ENTITY_ID))
            .collect(Collectors.toList()).contains(getValueFromFieldName(entity, ENTITY_ID))).collect(Collectors.toList());
    deleteList.forEach(entity -> {
      this.handleRelateAuditColumn(rootEntity, null, entity);
      List<Field> fieldsWithAnnotation = getFieldsWithAnnotation(entity, AuditColumn.class);
      AuditLogDataTypeEnum type = relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).type();
      AuditLogDataTypeEnum group = relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).group() == AuditLogDataTypeEnum.ANY ? type
          : relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).group();
      fieldsWithAnnotation.stream().filter(field ->
          Arrays.asList(field.getAnnotation(AuditColumn.class).on()).contains(AuditActionEnum.DELETE)
      ).forEach(field -> {
        Object fromValue = getValueFromFieldName(entity, field.getName());
        rootEntity.getAuditLogDataDTOList().add(getAuditLogDataDTO(
            new AuditableDTO(rootEntity, type, group, field, fromValue, null, AuditActionEnum.DELETE, getBusinessKey(entity))));
      });
    });
  }

  private void handleUpdateCase(JpaAuditableAuditLog rootEntity, Field relatedAuditColumnField, List<?> relatedEntityList,
      List<?> originalRelatedEntityList) {
    List<?> updateList = relatedEntityList.stream().filter(entity ->
        originalRelatedEntityList.stream().map(item -> getValueFromFieldName(item, ENTITY_ID))
            .collect(Collectors.toList()).contains(getValueFromFieldName(entity, ENTITY_ID))).collect(Collectors.toList());
    updateList.forEach(entity -> {
      Object originalRelatedEntity = originalRelatedEntityList.stream()
          .filter(item -> Objects.equals(getValueFromFieldName(entity, ENTITY_ID), getValueFromFieldName(item, ENTITY_ID)))
          .findFirst().orElse(null);
      this.handleRelateAuditColumn(rootEntity, entity, originalRelatedEntity);
      List<Field> fieldsWithAnnotation = getFieldsWithAnnotation(entity, AuditColumn.class);
      AuditLogDataTypeEnum type = relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).type();
      AuditLogDataTypeEnum group = relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).group() == AuditLogDataTypeEnum.ANY ? type
          : relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).group();
      fieldsWithAnnotation.stream().filter(field ->
          Arrays.asList(field.getAnnotation(AuditColumn.class).on()).contains(AuditActionEnum.UPDATE)
      ).forEach(field -> {
        Object toValue = getValueFromFieldName(entity, field.getName());
        Object fromValue = getValueFromFieldName(originalRelatedEntity, field.getName());
        rootEntity.getAuditLogDataDTOList().add(getAuditLogDataDTO(
            new AuditableDTO(rootEntity, type, group, field, fromValue, toValue, AuditActionEnum.UPDATE, getBusinessKey(entity))));
      });
    });
  }

  private void handleAddCase(JpaAuditableAuditLog rootEntity, Field relatedAuditColumnField, List<?> relatedEntityList,
      List<?> originalRelatedEntityList) {
    List<?> addList = relatedEntityList.stream().filter(entity ->
        !originalRelatedEntityList.stream().map(item -> getValueFromFieldName(item, ENTITY_ID))
            .collect(Collectors.toList()).contains(getValueFromFieldName(entity, ENTITY_ID))).collect(Collectors.toList());
    addList.forEach(entity -> {
      this.handleRelateAuditColumn(rootEntity, entity, null);
      List<Field> fieldsWithAnnotation = getFieldsWithAnnotation(entity, AuditColumn.class);
      AuditLogDataTypeEnum type = relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).type();
      AuditLogDataTypeEnum group = relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).group() == AuditLogDataTypeEnum.ANY ? type
          : relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).group();
      fieldsWithAnnotation.stream().filter(field ->
          Arrays.asList(field.getAnnotation(AuditColumn.class).on()).contains(AuditActionEnum.ADD)
      ).forEach(field -> {
        Object toValue = getValueFromFieldName(entity, field.getName());
        rootEntity.getAuditLogDataDTOList().add(getAuditLogDataDTO(
            new AuditableDTO(rootEntity, type, group, field, null, toValue, AuditActionEnum.ADD, getBusinessKey(entity))));
      });
    });
  }

  private AuditLogDataDTO getAuditLogDataDTO(JpaAuditableAuditLog jpaAuditable, AuditLogDataTypeEnum type, AuditLogDataTypeEnum group,
      Field auditColumnField,
      Object fromValue,
      Object toValue, AuditActionEnum action) {
    return this.getAuditLogDataDTO(
        new AuditableDTO(jpaAuditable, type, group, auditColumnField, fromValue, toValue, action, getBusinessKey(jpaAuditable)));
  }

  private AuditLogDataDTO getAuditLogDataDTO(AuditableDTO auditableDTO) {
  //处理threadLocal中带过来的remark
    Optional<AuditRemarkDTO> remarkDTO = getAuditRemarkDTO(auditableDTO.getAuditColumnField(), auditableDTO.getType());
    return AuditLogDataDTO.builder()
        .key(getKey(auditableDTO.getJpaAuditable(), getValueFromFieldName(auditableDTO.getJpaAuditable(), ENTITY_ID)))
        .businessKey(auditableDTO.getBusinessKey())
        .fieldName(auditableDTO.getAuditColumnField().getAnnotation(AuditColumn.class).value())
        .type(auditableDTO.getType())
        .group(auditableDTO.getGroup())
        .fromValue(auditableDTO.getFromValue())
        .action(auditableDTO.getAction())
        .toValue(auditableDTO.getToValue())
        .updateBy(StringUtils.defaultIfBlank(remarkDTO.orElse(AuditRemarkDTO.builder().build()).getTriggerBy(),
            auditableDTO.getJpaAuditable().getLastModifiedBy()))
        .triggerPoint(remarkDTO.orElse(AuditRemarkDTO.builder().build()).getTriggerPoint())
        .remark(remarkDTO.orElse(AuditRemarkDTO.builder().build()).getRemark())
        .build();
  }


  private Optional<AuditRemarkDTO> getAuditRemarkDTO(Field field, AuditLogDataTypeEnum type) {
    Optional<AuditRemarkDTO> remarkDTO = Optional.empty();
    AuditDTO auditDTO = AuditContextUtils.getContext();
    if (Objects.nonNull(auditDTO)) {
      remarkDTO = auditDTO.getRemarks().stream()
          .filter(remark -> (Objects.equals(remark.getType(), type) || Objects.equals(remark.getType(), AuditLogDataTypeEnum.ANY))
              && (StringUtils.equals(field.getName(), remark.getColumn()) || StringUtils.equals("[any]", remark.getColumn()))).findFirst();
    }
    return remarkDTO;
  }


  private void handleRelatedSingleEntity(JpaAuditableAuditLog rootEntity, Field relatedAuditColumnField, Object relatedEntity,
      Object originalRelatedEntity) {
    List<Field> fieldsWithAnnotation = getFieldsWithAnnotation(Objects.isNull(originalRelatedEntity) ? relatedEntity : originalRelatedEntity,
        AuditColumn.class);
    AuditLogDataTypeEnum type = relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).type();
    AuditLogDataTypeEnum group = relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).group() == AuditLogDataTypeEnum.ANY ? type
        : relatedAuditColumnField.getAnnotation(RelatedAuditColumn.class).group();
    fieldsWithAnnotation.forEach(field -> {
      if (Objects.nonNull(relatedEntity) && Objects.isNull(originalRelatedEntity) && Arrays.asList(field.getAnnotation(AuditColumn.class).on())
          .contains(AuditActionEnum.ADD)) {
        Object toValue = getValueFromFieldName(relatedEntity, field.getName());
        rootEntity.getAuditLogDataDTOList()
            .add(getAuditLogDataDTO(
                new AuditableDTO(rootEntity, type, group, field, null, toValue, AuditActionEnum.ADD, getBusinessKey(relatedEntity))));
      } else if (Objects.nonNull(originalRelatedEntity) && Objects.isNull(relatedEntity) && Arrays.asList(field.getAnnotation(AuditColumn.class).on())
          .contains(AuditActionEnum.DELETE)) {
        Object fromValue = getValueFromFieldName(originalRelatedEntity, field.getName());
        rootEntity.getAuditLogDataDTOList().add(getAuditLogDataDTO(
            new AuditableDTO(rootEntity, type, group, field, fromValue, null, AuditActionEnum.DELETE, getBusinessKey(originalRelatedEntity))));
      } else if (Arrays.asList(field.getAnnotation(AuditColumn.class).on()).contains(AuditActionEnum.UPDATE)) {
        Object fromValue = getValueFromFieldName(originalRelatedEntity, field.getName());
        Object toValue = getValueFromFieldName(relatedEntity, field.getName());
        rootEntity.getAuditLogDataDTOList().add(getAuditLogDataDTO(
            new AuditableDTO(rootEntity, type, group, field, fromValue, toValue, AuditActionEnum.UPDATE, getBusinessKey(relatedEntity))));
      }
    });

  }

  private void publishEvent(JpaAuditableAuditLog jpaAuditable) {
    ApplicationContext applicationContext = SpringApplicationContextUtil.getApplicationContext();
    if (Objects.isNull(applicationContext)) {
      return;
    }
    List<AuditLogEvent> auditLogEvents = jpaAuditable.generateAuditLogEvent();
    if (CollectionUtils.isEmpty(auditLogEvents)) {
      return;
    }
    applicationContext.publishEvent(AuditLogsEvent.builder().auditLogEvents(auditLogEvents).build());
  }

  private String getBusinessKey(Object obj) {
    List<Field> fieldsWithAnnotation = getFieldsWithAnnotation(obj, AuditBusinessKey.class);
    if (CollectionUtils.isEmpty(fieldsWithAnnotation)) {
      return null;
    }
    List<String> businessKeyList = new ArrayList<>();
    for (Field field : fieldsWithAnnotation) {
      PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(obj.getClass(), field.getName());
      if (Objects.isNull(propertyDescriptor)) {
        continue;
      }
      Object value = ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(), obj);
      businessKeyList.add(Objects.isNull(value) ? StringUtils.EMPTY : value.toString());
    }
    return businessKeyList.stream().filter(StringUtils::isNotBlank).collect(Collectors.joining(","));
  }

  private String getKey(Object obj, Object id) {
    Field field = getFieldWithAnnotation(obj, AuditKey.class);
    if (Objects.isNull(field)) {
      return String.valueOf(id);
    }
    AuditKey auditKey = field.getAnnotation(AuditKey.class);
    String spEL = auditKey.expression();
    PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(obj.getClass(), field.getName());
    if (Objects.isNull(propertyDescriptor)) {
      return null;
    }
    Object value = ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(), obj);
    if (StringUtils.isEmpty(spEL)) {
      return Objects.isNull(value) ? null : value.toString();
    }
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable(field.getName(), value);
    try {
      Expression expression = parser.parseExpression(spEL);
      return String.valueOf(expression.getValue(context, Object.class));
    } catch (Exception e) {
      log.error("AuditKey parse expression failed:[{}]", ExceptionUtils.getStackTrace(e));
      return null;
    }
  }

  @Data
  @AllArgsConstructor
  static final class AuditableDTO {

    private final JpaAuditableAuditLog jpaAuditable;
    private final AuditLogDataTypeEnum type;
    private final AuditLogDataTypeEnum group;
    private final Field auditColumnField;
    private final Object fromValue;
    private final Object toValue;
    private final AuditActionEnum action;
    private final String businessKey;
  }


}

@Data
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@MappedSuperclass
@EntityListeners(JpaAuditableLogListener.class)
public class JpaAuditableAuditLog extends JpaAuditable implements Serializable {

  private static final long serialVersionUID = 8984023283230901419L;
  @Transient
  @Builder.Default
  private List<AuditLogDataDTO> auditLogDataDTOList = new ArrayList<>();


  public List<AuditLogEvent> generateAuditLogEvent() {
    if (CollectionUtils.isEmpty(this.auditLogDataDTOList)) {
      return Collections.emptyList();
    }
    // 过滤出来有变化的数据
    List<AuditLogDTO> auditLogDTOList = this.auditLogDataDTOList.stream()
        .filter(Objects::nonNull).filter(AuditLogDataDTO::isDataChanged)
        .filter(item -> StringUtils.isNotBlank(item.getKey())).map(AuditLogDataDTO::toAuditLogDto)
        .collect(Collectors.toList());
    if (CollectionUtils.isEmpty(auditLogDTOList)) {
      return Collections.emptyList();
    }
    
    //进行分组
    Map<String, List<AuditLogDTO>> auditLogDTOGroup = auditLogDTOList.stream()
        .collect(Collectors.groupingBy(item -> Optional.ofNullable(item.getGroup())
            .orElse(Optional.ofNullable(item.getType()).orElse(AuditLogDataTypeEnum.ANY)).name()));
    return auditLogDTOGroup.values().stream().map(auditLogDTOs -> {
      AuditLogDTO auditLogDTO = auditLogDTOs.stream().findFirst().orElse(AuditLogDTO.builder().build());
      return AuditLogEvent.builder().partitionKey(auditLogDTO.getKey())
          .key(auditLogDTO.getKey())
          .businessKey(
              auditLogDTOs.stream().map(AuditLogDTO::getBusinessKey).filter(StringUtils::isNotBlank).distinct().collect(Collectors.joining(",")))
          .updateDateTime(Optional.ofNullable(auditLogDTO.getUpdateDateTime()).orElse(LocalDateTime.now()))
          .action(auditLogDTOs.stream().filter(item -> Objects.nonNull(item.getAction())).map(item -> item.getAction().name()).distinct()
              .collect(Collectors.joining(",")))
          .updateBy(auditLogDTO.getUpdateBy())
          .type(auditLogDTO.getGroup())
          .triggerPoint(
              auditLogDTOs.stream().map(AuditLogDTO::getTriggerPoint).filter(StringUtils::isNotBlank).distinct().collect(Collectors.joining(",")))
          .remark(auditLogDTOs.stream().map(AuditLogDTO::getRemark).filter(StringUtils::isNotBlank).distinct().collect(Collectors.joining(",")))
          .auditLogDTOList(
              auditLogDTOs)
          .eventCreatedTime(LocalDateTime.now())
          .build();
    }).collect(Collectors.toList());

  }
}

最后会把这些event发送到日志服务中,@TransactionalEventListener会在事务提交之后触发,然后会remove掉threadLocal中context的一些信息

@Component
@Slf4j
@RequiredArgsConstructor
public class AuditLogEventListener {

  private final AuditLogPublisherService auditLogPublisherService;


  @TransactionalEventListener
  public void handleAuditLogEvent(AuditLogsEvent auditLogEvents) {
    AuditContextUtils.remove();
    auditLogEvents.getAuditLogEvents().forEach(auditLogPublisherService::publishAuditLog);
    log.info("send auditLog event: {}", JsonUtils.toJson(auditLogEvents));
  }

}

然后接下来是remark的处理,使用切面的方式进行拦截,然后把数据存入到threadLocal中供后面使用,然后在最后事务提交完毕之后会清理掉这些threadLocal中的数据。

@Component
@Aspect
@Slf4j
public class AuditLogAspect {

  private final ExpressionParser parser = new SpelExpressionParser();
  private final LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();


  @Around("@annotation(com.xxx.common.annotation.AuditLog)")
  public Object invoked(ProceedingJoinPoint point) throws Throwable {
    Object[] args = point.getArgs();
    Method method = ((MethodSignature) point.getSignature()).getMethod();
    if (Objects.isNull(method) || !method.isAnnotationPresent(AuditLog.class)) {
      return point.proceed();
    }
    AuditLog auditLog = method.getAnnotation(AuditLog.class);
    AuditRemark[] auditRemarks = auditLog.remarks();
    List<AuditRemarkDTO> remarks = new ArrayList<>();
    for (AuditRemark auditRemark : auditRemarks) {
      if (Boolean.TRUE.equals(this.parseSpEL(method, args, auditRemark.condition(), Boolean.class, Boolean.TRUE))) {
        remarks.add(getAuditRemarkDTO(auditRemark, method, args));
      }
    }
    remarks = remarks.stream().filter(Objects::nonNull).collect(Collectors.toList());
    if (CollectionUtils.isEmpty(remarks)) {
      return point.proceed();
    }
    try {
      AuditContextUtils.setContext(AuditDTO.builder()
          .remarks(remarks).build());
      return point.proceed();
    } finally {
      AuditEventUtils.publishEvent(AuditContextUtils.getContext());
    }
  }


  private <T> T parseSpEL(Method method, Object[] arguments, String spEL, Class<T> clazz, T defaultResult) {
    if (StringUtils.isBlank(spEL)) {
      return defaultResult;
    }
    String[] params = discoverer.getParameterNames(method);
    if (Objects.isNull(params)) {
      return defaultResult;
    }
    EvaluationContext context = new StandardEvaluationContext();
    for (int len = 0; len < params.length; len++) {
      context.setVariable(params[len], arguments[len]);
    }
    try {
      Expression expression = parser.parseExpression(spEL);
      return expression.getValue(context, clazz);
    } catch (Exception e) {
      return defaultResult;
    }
  }


  private AuditRemarkDTO getAuditRemarkDTO(AuditRemark auditRemark, Method method, Object[] arguments) {
    String triggerPoint = StringUtils.defaultIfBlank(auditRemark.value(),
        auditRemark.triggerPoint());
    if (StringUtils.isEmpty(triggerPoint)) {
      triggerPoint = this.parseSpEL(method, arguments, auditRemark.triggerPointExpression(), String.class,
          StringUtils.EMPTY);
    }
    String remark = auditRemark.remark();
    if (StringUtils.isEmpty(remark)) {
      remark = this.parseSpEL(method, arguments, auditRemark.remarkExpression(), String.class,
          StringUtils.EMPTY);
    }
    String triggerBy = auditRemark.triggerBy();
    if (StringUtils.isEmpty(triggerBy)) {
      triggerBy = this.parseSpEL(method, arguments, auditRemark.triggerByExpression(), String.class,
          StringUtils.EMPTY);
    }
    if (StringUtils.isNotEmpty(triggerPoint) || StringUtils.isNotEmpty(remark) || StringUtils.isNotEmpty(triggerBy)) {
      return AuditRemarkDTO.builder()
          .remark(remark)
          .triggerPoint(triggerPoint)
          .triggerBy(triggerBy)
          .type(auditRemark.type()).column(auditRemark.column()).build();
    }
    return null;
  }


  @AfterThrowing(throwing = "ex", pointcut = "@annotation(com.xxx.common.annotation.AuditLog)")
  private void afterThrowing(Throwable ex) {
    AuditContextUtils.remove();
    log.error("AuditRemarkAspect catch exception:[{}]", ex.getMessage());
  }


}

清理掉context中的数据,@TransactionalEventListener会在事务提交后执行

@Component
@Slf4j
@RequiredArgsConstructor
public class AuditRemarkEventListener {


  @TransactionalEventListener(fallbackExecution = true)
  public void handleRemarkEvent(AuditDTO auditDTO) {
    AuditContextUtils.remove();
    log.info("send auditDTO event: {}", JsonUtils.toJson(auditDTO));
  }


}

一些工具类

@UtilityClass
public class ReflexUtils {


  public static Object getValueFromFieldName(Object obj, String name) {
    if (Objects.isNull(obj) || StringUtils.isEmpty(name)) {
      return null;
    }
    PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(obj.getClass(), name);
    if (Objects.nonNull(propertyDescriptor)) {
      return ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(), obj);
    }
    return null;
  }


  public static List<Field> getFieldsWithAnnotation(Object obj, Class<? extends Annotation> annotation) {
    if (Objects.isNull(obj)) {
      return Collections.emptyList();
    }
    return getFieldsWithAnnotation(obj.getClass(), annotation);
  }

  public static List<Field> getFieldsWithAnnotation(Class<?> clazz, Class<? extends Annotation> annotation) {
    List<Field> fieldList = new ArrayList<>();
    ReflectionUtils.doWithFields(clazz, field -> {
      if (field.isAnnotationPresent(annotation)) {
        fieldList.add(field);
      }
    });
    return fieldList;
  }

  public static Field getFieldWithAnnotation(Object obj, Class<? extends Annotation> annotation) {
    if (Objects.isNull(obj)) {
      return null;
    }
    List<Field> fieldList = new ArrayList<>();
    ReflectionUtils.doWithFields(obj.getClass(), field -> {
      if (field.isAnnotationPresent(annotation)) {
        fieldList.add(field);
      }
    });
    if (CollectionUtils.isEmpty(fieldList)) {
      return null;
    }
    return fieldList.get(0);
  }

}
@Slf4j
@UtilityClass
public class AuditContextUtils {

  private static final ThreadLocal<AuditDTO> auditContext = new ThreadLocal<>();

  public static AuditDTO getContext() {
    return auditContext.get();
  }

  public static void remove() {
    auditContext.remove();
    log.info("remove AuditContext..");
  }

  public static void setContext(AuditDTO auditDTO) {
    auditContext.set(auditDTO);
    log.info("set AuditContext:[{}]", JsonUtils.toJson(auditDTO));
  }
}

@Component
@Slf4j
public class AuditEventUtils implements ApplicationContextAware {

  private static ApplicationContext applicationContext;

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    AuditEventUtils.applicationContext = applicationContext; //NOSONAR
  }


  public static ApplicationContext getApplicationContext() {
    return AuditEventUtils.applicationContext;
  }

  public static void publishEvent(AuditDTO auditDTO) {
    if (Objects.isNull(auditDTO)) {
      return;
    }
    AuditEventUtils.applicationContext.publishEvent(auditDTO);
    log.info("publish auditDTO event:[{}]", JsonUtils.toJson(auditDTO));
  }

}

代码比较多,可能有些地方不太好理解,但总的来说思路是比较清晰的,就是比较出实体中那些字段产生了变化。

但是里面也是存在一些坑的。比如在一对多的实体中,我们监听了一的一方,然后我们只修改了多一方中的实体数据,这个时候并不会触发到一的一方的监听器。这个时候我们可以用一个list的变量引用多一方的数据,然后把list清空掉,最后在把数据添加回去,这样就可以触发一的一方的监听器。

List<XXXEntity> xxxEntities = new ArrayList<>(this.getXXXList());
this.getXXXList().clear();
this.getXXXList().addAll(xxxEntities);

本文内容和代码比较多,也涉及到了项目中的一些具体实现,所以不会全部展示出来,读者可以更多关注设计和实现思路,代码只是作为辅助思考,希望能帮到有需要的读者,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值