之前的文章实现了自定义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
,只要在保存文档之前将数据拦截处理就可以实现字段填充。