一招解决 SpringDataMongodb 审计(统一维护公共字段)功能

2,042 阅读5分钟

缘起

最近在公司使用 MongoDB 这种文档数据库,和 MySQL 一样在设计表实体时候都有一些公有的字段,比如,创建人、更新人、创建时间、更新时间。

那么 MongoDB 如何统一维护这些字段呢?

其实有两种方式:

  • SpringDataJpa 审计
    在使用 MySQL 的时候,我们经常使用 springDataJpa 作为数据库持久层框架,我们知道 jpa 有审计功能,那 springDataMongoDB 作为 springData 下的产物,它也是可以使用 jpa 的审计功能的,具体使用方式可以百度。

  • SpringDataMongoDB 生命周期事件
    SpringDataMongoDB 发布⼀些⾮常有⽤的⽣命周期事件,比如实体转换之前,实体保存之前,实体保存之后,实体加载之后等都有对应的钩子函数,我们可以在这些钩子函数里面统一维护公共字段。

问题

MongoDB 维护实体统一的字段,在上面介绍了两种方式,但是他们都有缺陷:

利用 SpringDataJpa 审计的方式,只有继承自 MongoRepositoryRepository接口,它们的写入和修改可以使用审计功能,如果你使用 MongoTemplate 来保存和修改是不能自动维护公共字段的。

而使用 SpringDataMongoDB 的生命周期函数无论是使用 Repository 接口来保存或者修改,还是使用 MongoTemplate 的保存和修改都能走到钩子函数,然后我们自己在钩子函数中统一维护。

因为我们写的 Repository 是继承 MongoRepository ,具体实现是 SimpleMongoRepository, 翻开 SimpleMongoRepository 的源码: image.png 保存数据是通过 MongoOperations 来实现的,而 MongoTemplate 正是 MongoOperations 的实现。

那么,是不是只要使用 SpringDataMongoDB 的生命周期函数就行了?其实还有一个问题,就是我们使用 MongoTemplate 工具类中以 update 开头的API,比如 updateFirst,还有 upsert 等API是不走钩子函数的。

翻开 MongoTemplate 的源码发现: image.png 它们都统一调用 doUpdate 方法, image.png 这个方法并没有增加生命周期事件的调用。我们在看 MongoTemplatesave 方法, image.png 可以看到这个 doSave 方法中有 和保存相关的生命周期事件。

解决思路

那么对应上面说的问题,该如何解决呢?

第一个想法是改写工具类,在 update 之前主动维护公共字段,但是这种方式代价太大了,而且也不符合 “开闭原则”,那这种方式行不通,不能修改,就只能考虑扩展了。

扩展的思路就是怎么在使用 MongoTemplateupdate 方法之前或者之后做一些我们的逻辑。可以想到的很简单的方式就是 代理模式

我们通过 代理一个 MongoTemplate 工具类,在调用 MongoTemplateupdate 方法之前维护自己的公共字段。

方案实现

  • 实现 SpringDataMongoDB 的生命周期事件

    监听 MongoDB 的生命周期,只需重写org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener的子类,这样在分派事件时会调用它们。
    AbstractMappingEventListener中提供了以下回调方法:

    方法描述
    onBeforeConvert调用MongoTemplateinsertinsertListsave操作,在通过MongoConverter将对象转换为文档之前的处理。
    onBeforeSave调用MongoTemplateinsertinsertListsave操作,在数据库中插入或保存文档之前的处理。
    onAfterSave调用MongoTemplateinsertinsertListsave操作,在数据库中插入或保存文档之后的处理。
    onAfterLoad调用MongoTemplate中的findfindAndRemovefindOnegetCollection方法,从数据库检索文档后的处理。
    onAfterConvert调用MongoTemplate中的findfindAndRemovefindOnegetCollection方法,从数据库检索文档被转换为POJO后的处理。

    这里值得注意的是: 我们这里要使用 onBeforeConvert 这个回调,我一开始使用 onBeforeSave 这个回调,发现设置的公共字段都没有数据,原因是 onBeforeConvert 这个回调,会将实体转为 Document 文档,那么就是先设置实体,然后将实体转 Document,然后把 Document 写入数据库,这样回调设置的数据就转到 Document 对象中了。 而调用 onBeforeSave 这个回调时候已经将实体对象转完 Document 了,这样即使再设置 实体对象也没用了。当然如果使用这个回调可以直接修改 Document 对象内容,我想应该也是可以的,我就不试了。

    MongoTemplate.doInsert 源码image.png

    完整代码

    package com.bokbok.content.audit;
    
    import com.bokbok.common.vo.app.AccountInfoVO;
    import com.bokbok.core.utils.HttpServletRequestHelper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    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.data.mongodb.core.mapping.event.BeforeSaveEvent;
    import org.springframework.stereotype.Component;
    import org.springframework.util.ReflectionUtils;
    
    import java.lang.reflect.Field;
    import java.util.Optional;
    
    @Component
    @Slf4j
    public class AuditingListener extends AbstractMongoEventListener<Object> {
    
        // 获取用户的工具类 看自己逻辑修改
        @Autowired
        private HttpServletRequestHelper httpServletRequestHelper;
    
        @Override
        public void onBeforeConvert(BeforeConvertEvent<Object> event) {
            log.info("On before Save inserting the record to the Delete and update collection");
    
            Object source = event.getSource();
            if(source == null){
                return;
            }
    
            long time = System.currentTimeMillis();
    
            Field idField = ReflectionUtils.findField(source.getClass(), "id");
            if(idField != null && valueIsNotEmpty(source, idField)){
                // 更新
                ReflectionUtils.doWithFields(source.getClass(), field -> {
                    handleLastModifiedDate(source, field, time);
                    handleLastModifiedBy(source, field);
                });
    
            } else {
                // 增加
                ReflectionUtils.doWithFields(source.getClass(), field -> {
                    handleCreatedBy(source, field);
                    handleCreatedDate(source, field, time);
                    handleLastModifiedDate(source, field, time);
                    handleLastModifiedBy(source, field);
                });
            }
    
        }
    
        // 判断字段 是否有值
        private boolean valueIsNotEmpty(Object source, Field idField){
            ReflectionUtils.makeAccessible(idField);
            return ReflectionUtils.getField(idField, source) != null;
        }
    
        private void handleCreatedBy(Object source, Field field) throws IllegalAccessException {
            CreatedBy createdBy = AnnotationUtils.getAnnotation(field, CreatedBy.class);
            if(createdBy != null){
                AccountInfoVO accountInfo = httpServletRequestHelper.getAccountInfo();
                if(accountInfo != null){
                    field.setAccessible(true);
                    field.set(source, String.valueOf(accountInfo.getId()));
                }
            }
        }
    
        private void handleCreatedDate(Object source, Field field, long time) throws IllegalAccessException {
            CreatedDate createdDate = AnnotationUtils.getAnnotation(field, CreatedDate.class);
            if(createdDate != null){
                field.setAccessible(true);
                field.set(source, time);
            }
        }
    
        private void handleLastModifiedBy(Object source, Field field) throws IllegalAccessException {
            LastModifiedBy lastModifiedBy = AnnotationUtils.getAnnotation(field, LastModifiedBy.class);
            if(lastModifiedBy != null){
                AccountInfoVO accountInfo = httpServletRequestHelper.getAccountInfo();
                if(accountInfo != null){
                    field.setAccessible(true);
                    field.set(source, String.valueOf(accountInfo.getId()));
                }
            }
        }
    
        private void handleLastModifiedDate(Object source, Field field, long time) throws IllegalAccessException {
            LastModifiedDate lastModifiedDate = AnnotationUtils.getAnnotation(field, LastModifiedDate.class);
            if(lastModifiedDate != null){
                field.setAccessible(true);
                field.set(source, time);
            }
        }
    
    
    }
    

    实体对象 :这几个注解是 springdata
    image.png

  • 通过代理模式代理一个增强的工具类

    package com.bokbok.content.audit;
    
    import com.bokbok.common.vo.app.AccountInfoVO;
    import com.bokbok.core.utils.HttpServletRequestHelper;
    import com.mongodb.client.result.UpdateResult;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.annotation.AnnotationUtils;
    import org.springframework.data.annotation.LastModifiedBy;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.mongodb.MongoDbFactory;
    import org.springframework.data.mongodb.core.MongoTemplate;
    import org.springframework.data.mongodb.core.query.Query;
    import org.springframework.data.mongodb.core.query.Update;
    import org.springframework.lang.Nullable;
    import org.springframework.util.ReflectionUtils;
    
    @Slf4j
    public class MongoTemplateProxy extends MongoTemplate {
    
        private HttpServletRequestHelper httpServletRequestHelper;
    
        public MongoTemplateProxy(MongoDbFactory mongoDbFactory, HttpServletRequestHelper httpServletRequestHelper) {
            super(mongoDbFactory);
            this.httpServletRequestHelper = httpServletRequestHelper;
        }
    
        @Override
        protected UpdateResult doUpdate(String collectionName, Query query, Update update, @Nullable Class<?> entityClass, boolean upsert, boolean multi) {
            log.info("-------MongoTemplateProxy---doUpdate----");
            ReflectionUtils.doWithFields(entityClass, field -> {
                LastModifiedDate lastModifiedDate = AnnotationUtils.getAnnotation(field, LastModifiedDate.class);
                if(lastModifiedDate != null){
                    update.set(field.getName(), System.currentTimeMillis());
                }
    
                LastModifiedBy lastModifiedBy = AnnotationUtils.getAnnotation(field, LastModifiedBy.class);
                if(lastModifiedBy != null){
                    AccountInfoVO accountInfo = httpServletRequestHelper.getAccountInfo();
                    if(accountInfo != null){
                        update.set(field.getName(), String.valueOf(accountInfo.getId()));
                    }
                }
            });
            return super.doUpdate(collectionName, query, update, entityClass, upsert, multi);
        }
    }
    

好了,以上就是完成 SpringDataMongodb 的审计功能。