Spring boot MongoDB 实现自定义审计字段

	之前的文章实现了自定义Repository基类和业务基类,现在有了新的需求,就是一些公共字段的填充,例如:创建时间、更新时间、创建人、更新人等字段,`spring-boot-starter-data-mongodb`中已经提供类似的审计注解,例如:`@CreatedDate`、`@CreatedBy`、`@LastModifiedDate`、`@LastModifiedBy`,但是这些注解只能在Repository的接口中使用,也就是说只能在JPA场景下使用,例如使用了`MongoTemplate`就无法使用这些注解,而且这些注解并不能满足我们实际的业务场景,有时需要自定义审计字段,例如多租户下的租户ID。

AuditorAware是什么?

AuditorAware是Spring Data提供的一个接口,用于提供当前执行数据库操作的"审计员"的信息。"审计员"可以是当前操作的用户、系统的默认用户或其他相关信息,用于记录和跟踪数据的变更历史。

具体来说,AuditorAware的作用是为实体类中标记了@CreatedBy@LastModifiedBy注解的属性提供值。

AuditorAware接口有一个方法:

Optional<T> getCurrentAuditor();

我们只需要重写此方法即可,假设我们的用户ID为String类型,具体操作如下:

public class MongoAuditorAware implements AuditorAware<String> {
    @NotNull
    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of(UserContext.getUserId());//此处设置系统的用户唯一标志或其他标志字段
    }
}

使MongoTemplate也支持@CreatedDate@CreatedBy@LastModifiedDate@LastModifiedBy

自定义AuditingMongoEventListener.java继承AbstractMongoEventListener并重写onBeforeConvert方法。

import com.learning.mongodb.crud.annotations.TenantId;
import com.learning.mongodb.crud.constant.MongodbConstant;
import com.learning.mongodb.crud.helper.UserContext;
import lombok.SneakyThrows;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.Objects;

/**
 * 自定义审计字段方式一:mongoDB审计字段监听
 */
@Component
public class AuditingMongoEventListener extends AbstractMongoEventListener {


    @SneakyThrows
    @Override
    public void onBeforeConvert(BeforeConvertEvent event) {
        Object source = event.getSource();
        Field id = ReflectionUtils.findField(source.getClass(), MongodbConstant.ID);
        Date date = new Date();
        if (Objects.nonNull(id) && valueIsNotEmpty(source, id)) {
            //修改
            ReflectionUtils.doWithFields(source.getClass(), field -> {
                handleLastModifiedDate(source, field, date);
                handleLastModifiedBy(source, field);
            });
        } else {
            //新增
            ReflectionUtils.doWithFields(source.getClass(), field -> {
                handleCreatedBy(source, field);
                handleCreatedDate(source, field, date);
                handleLastModifiedDate(source, field, date);
                handleLastModifiedBy(source, field);
                handleTenantId(source, field);
            });
        }
    }

    private void handleCreatedDate(Object source, Field field, Date time) throws IllegalAccessException {
        if (canBeFilled(field,CreatedDate.class)) {
            field.setAccessible(true);
            field.set(source, time);
        }
    }

    private void handleCreatedBy(Object source, Field field) throws IllegalAccessException {
        if (canBeFilled(field,CreatedBy.class)) {
            field.setAccessible(true);
            field.set(source, UserContext.getUserId());
        }
    }

    private void handleTenantId(Object source, Field field) throws IllegalAccessException {
        if (canBeFilled(field,TenantId.class)) {
            field.setAccessible(true);
            field.set(source, UserContext.getTenantId());
        }
    }

    private void handleLastModifiedBy(Object source, Field field) throws IllegalAccessException {
        if (canBeFilled(field,LastModifiedBy.class)) {
            field.setAccessible(true);
            field.set(source, UserContext.getUserId());
        }
    }

    private void handleLastModifiedDate(Object source, Field field, Date time) throws IllegalAccessException {
        if (canBeFilled(field,LastModifiedDate.class)) {
            field.setAccessible(true);
            field.set(source, time);
        }
    }


    /**
     * 判断属性是否为空
     *
     * @param source 对象
     * @param field  对象属性
     * @return 不为空
     * @throws IllegalAccessException 异常
     */
    private boolean valueIsNotEmpty(Object source, Field field) throws IllegalAccessException {
        ReflectionUtils.makeAccessible(field);
        return Objects.nonNull(field.get(source));
    }

    /**
     * 是否可以填充值
     * @param field 属性
     * @param annotationType 注解类型
     * @return 是否可以填充
     */
    private boolean canBeFilled(Field field, Class<? extends Annotation> annotationType) {
        return Objects.nonNull(AnnotationUtils.getAnnotation(field, annotationType));
    }
}

除了上面这种方式,还可以通过实现BeforeConvertCallback类并重写onBeforeConvert方法,用法和继承AbstractMongoEventListener是一样的,示例代码如下:

import com.learning.mongodb.crud.annotations.TenantId;
import com.learning.mongodb.crud.constant.MongodbConstant;
import com.learning.mongodb.crud.helper.UserContext;
import com.sun.istack.internal.NotNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.Date;
import java.util.Objects;

/**
 * 保存修改之前数据处理
 */
@Slf4j
@Component
public class BeforeConvert implements BeforeConvertCallback<Object> {

   @SneakyThrows
    @NotNull
    @Override
    public Object onBeforeConvert(@NotNull Object source, @NotNull String s) {
        Field id = ReflectionUtils.findField(source.getClass(), MongodbConstant.ID);
        Date date = new Date();
        if (Objects.nonNull(id) && valueIsNotEmpty(source, id)) {
            //修改
            ReflectionUtils.doWithFields(source.getClass(), field -> {
                handleLastModifiedDate(source, field, date);
                handleLastModifiedBy(source, field);
            });
        } else {
            //新增
            ReflectionUtils.doWithFields(source.getClass(), field -> {
                handleCreatedBy(source, field);
                handleCreatedDate(source, field, date);
                handleLastModifiedDate(source, field, date);
                handleLastModifiedBy(source, field);
                handleTenantId(source, field);
            });
        }
        return source;
    }
	//...
}

使用

自定义租户注解TenantId.java

@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
public @interface TenantId {
}

UserContext.java

public class UserContext {
    /**
     * 获取系统唯一标志
     * @return 用户ID
     */
    public static String getUserId() {
        return "admin";
    }

    /**
     * 获取租户ID
     * @return 租户ID
     */
    public static String getTenantId() {
        return "tenant1";
    }
}

Book.java

@Document(collection = "Books")
@Data
public class Book {
    @Id
    private ObjectId id;
    @CreatedDate
    private Date createDate;
    @CreatedBy
    private String createBy;
    @LastModifiedDate
    private Date modifiedDate;
    @LastModifiedBy
    private String modifiedBy;
    @TenantId
    private String tenantId;
}

验证

curl --location 'http://localhost:8080/book' \
--header 'Content-Type: application/json' \
--data '{
    "name":"C Primer Plus 第9版",
    "price":53.9
}'

结果

{
    "id": "64a29a683e4b3f0f3a6b491f",
    "name": "C Primer Plus 第9版",
    "price": 53.9,
    "createDate": "2023-07-03T09:52:40.604+00:00",
    "createBy": "admin",
    "modifiedDate": "2023-07-03T09:52:40.604+00:00",
    "modifiedBy": "admin",
    "tenantId": "tenant1"
}

总结

无论使用MongoRepository还是MongoTemplate,只要在保存文档之前将数据拦截处理就可以实现字段填充。

你可能感兴趣的:(系统架构,Mongodb,开发总结,spring,boot,mongodb,python)