Bootstrap

超全分析MybatisPlus中的MetaObjectHandler全局字段填充的基本知识(附Demo及实战)

前言

对于Java的相关知识推荐阅读:

  1. java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)
  2. 【Java项目】实战CRUD的功能整理(持续更新)

MetaObjectHandler 是 MyBatis-Plus 提供的一个接口,用于处理在插入更新操作时的一些公共字段的自动填充

在这里插入图片描述

比如,在插入记录时自动填充 createTime 字段,在更新记录时自动填充 updateTime 字段
(一开始实战过程中莫名其妙的填充了这两个数据值,但是查找数据库触发器确没有这个,好奇哪里触发的,给我上了一课,对此详细研究并且科普该知识点)

1. 源码及API

通过源码分析各个方法:

/*
 * Copyright (c) 2011-2023, baomidou ([email protected]).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baomidou.mybatisplus.core.handlers;

import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.reflection.MetaObject;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

/**
 * 元对象字段填充控制器抽象类,实现公共字段自动写入<p>
 * <p>
 * 所有入参的 MetaObject 必定是 entity 或其子类的 MetaObject
 *
 * @author hubin
 * @since 2016-08-28
 */
public interface MetaObjectHandler {

    /**
     * 是否开启了插入填充
     */
    default boolean openInsertFill() {
        return true;
    }

    /**
     * 是否开启了更新填充
     */
    default boolean openUpdateFill() {
        return true;
    }

    /**
     * 插入元对象字段填充(用于插入时对公共字段的填充)
     *
     * @param metaObject 元对象
     */
    void insertFill(MetaObject metaObject);

    /**
     * 更新元对象字段填充(用于更新时对公共字段的填充)
     *
     * @param metaObject 元对象
     */
    void updateFill(MetaObject metaObject);

    /**
     * 通用填充
     *
     * @param fieldName  java bean property name
     * @param fieldVal   java bean property value
     * @param metaObject meta object parameter
     */
    default MetaObjectHandler setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject) {
        if (Objects.nonNull(fieldVal) && metaObject.hasSetter(fieldName)) {
            metaObject.setValue(fieldName, fieldVal);
        }
        return this;
    }

    /**
     * get value from java bean by propertyName
     *
     * @param fieldName  java bean property name
     * @param metaObject parameter wrapper
     * @return 字段值
     */
    default Object getFieldValByName(String fieldName, MetaObject metaObject) {
        return metaObject.hasGetter(fieldName) ? metaObject.getValue(fieldName) : null;
    }

    /**
     * find the tableInfo cache by metaObject </p>
     * 获取 TableInfo 缓存
     *
     * @param metaObject meta object parameter
     * @return TableInfo
     * @since 3.3.0
     */
    default TableInfo findTableInfo(MetaObject metaObject) {
        return TableInfoHelper.getTableInfo(metaObject.getOriginalObject().getClass());
    }

    /**
     * @param metaObject metaObject meta object parameter
     * @return this
     * @since 3.3.0
     */
    default <T, E extends T> MetaObjectHandler strictInsertFill(MetaObject metaObject, String fieldName, Class<T> fieldType, E fieldVal) {
        return strictInsertFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldType, fieldVal)));
    }

    /**
     * @param metaObject metaObject meta object parameter
     * @return this
     * @since 3.3.0
     */
    default <T, E extends T> MetaObjectHandler strictInsertFill(MetaObject metaObject, String fieldName, Supplier<E> fieldVal, Class<T> fieldType) {
        return strictInsertFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldVal, fieldType)));
    }

    /**
     * @param metaObject metaObject meta object parameter
     * @return this
     * @since 3.3.0
     */
    default MetaObjectHandler strictInsertFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
        return strictFill(true, tableInfo, metaObject, strictFills);
    }

    /**
     * @param metaObject metaObject meta object parameter
     * @return this
     * @since 3.3.0
     */
    default <T, E extends T> MetaObjectHandler strictUpdateFill(MetaObject metaObject, String fieldName, Supplier<E> fieldVal, Class<T> fieldType) {
        return strictUpdateFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldVal, fieldType)));
    }

    /**
     * @param metaObject metaObject meta object parameter
     * @return this
     * @since 3.3.0
     */
    default <T, E extends T> MetaObjectHandler strictUpdateFill(MetaObject metaObject, String fieldName, Class<T> fieldType, E fieldVal) {
        return strictUpdateFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldType, fieldVal)));
    }

    /**
     * @param metaObject metaObject meta object parameter
     * @return this
     * @since 3.3.0
     */
    default MetaObjectHandler strictUpdateFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
        return strictFill(false, tableInfo, metaObject, strictFills);
    }

    /**
     * 严格填充,只针对非主键的字段,只有该表注解了fill 并且 字段名和字段属性 能匹配到才会进行填充(null 值不填充)
     *
     * @param insertFill  是否验证在 insert 时填充
     * @param tableInfo   cache 缓存
     * @param metaObject  metaObject meta object parameter
     * @param strictFills 填充信息
     * @return this
     * @since 3.3.0
     */
    default MetaObjectHandler strictFill(boolean insertFill, TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
        if ((insertFill && tableInfo.isWithInsertFill()) || (!insertFill && tableInfo.isWithUpdateFill())) {
            strictFills.forEach(i -> {
                final String fieldName = i.getFieldName();
                final Class<?> fieldType = i.getFieldType();
                tableInfo.getFieldList().stream()
                    .filter(j -> j.getProperty().equals(fieldName) && fieldType.equals(j.getPropertyType()) &&
                        ((insertFill && j.isWithInsertFill()) || (!insertFill && j.isWithUpdateFill()))).findFirst()
                    .ifPresent(j -> strictFillStrategy(metaObject, fieldName, i.getFieldVal()));
            });
        }
        return this;
    }

    /**
     * 填充策略,默认有值不覆盖,如果提供的值为null也不填充
     *
     * @param metaObject metaObject meta object parameter
     * @param fieldName  java bean property name
     * @param fieldVal   java bean property value of Supplier
     * @return this
     * @since 3.3.0
     */
    default MetaObjectHandler fillStrategy(MetaObject metaObject, String fieldName, Object fieldVal) {
        if (getFieldValByName(fieldName, metaObject) == null) {
            setFieldValByName(fieldName, fieldVal, metaObject);
        }
        return this;
    }

    /**
     * 严格模式填充策略,默认有值不覆盖,如果提供的值为null也不填充
     *
     * @param metaObject metaObject meta object parameter
     * @param fieldName  java bean property name
     * @param fieldVal   java bean property value of Supplier
     * @return this
     * @since 3.3.0
     */
    default MetaObjectHandler strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) {
        if (metaObject.getValue(fieldName) == null) {
            Object obj = fieldVal.get();
            if (Objects.nonNull(obj)) {
                metaObject.setValue(fieldName, obj);
            }
        }
        return this;
    }
}

对应的代码都较为简单,此处以API的概念属性以及如何调用进行讲解

方法名概念作用参数调用方式
openInsertFill()检查是否开启插入填充返回布尔值,表示是否在插入时进行字段填充boolean open = metaObjectHandler.openInsertFill();
openUpdateFill()检查是否开启更新填充返回布尔值,表示是否在更新时进行字段填充boolean open = metaObjectHandler.openUpdateFill();
insertFill(MetaObject metaObject)插入操作时填充元对象字段定义插入时的字段自动填充逻辑MetaObject metaObject:元对象metaObjectHandler.insertFill(metaObject);
updateFill(MetaObject metaObject)更新操作时填充元对象字段定义更新时的字段自动填充逻辑MetaObject metaObject:元对象metaObjectHandler.updateFill(metaObject);
setFieldValByName(String fieldName, Object fieldVal, MetaObject metaObject)通用字段填充方法根据字段名设置字段值- String fieldName: 字段名
- Object fieldVal: 字段值
- MetaObject metaObject: 元对象
metaObjectHandler.setFieldValByName(“fieldName”, fieldVal, metaObject);
getFieldValByName(String fieldName, MetaObject metaObject)获取字段值根据字段名获取字段值- String fieldName: 字段名
- MetaObject metaObject: 元对象
Object value = metaObjectHandler.getFieldValByName(“fieldName”, metaObject);
findTableInfo(MetaObject metaObject)获取表信息根据元对象查找表信息缓存MetaObject metaObject: 元对象TableInfo tableInfo = metaObjectHandler.findTableInfo(metaObject);
strictInsertFill(MetaObject metaObject, String fieldName, Class fieldType, E fieldVal)严格模式插入填充在插入操作时严格填充指定字段- MetaObject metaObject: 元对象
- String fieldName: 字段名
- Class fieldType: 字段类型
- E fieldVal: 字段值
metaObjectHandler.strictInsertFill(metaObject, “fieldName”, fieldType, fieldVal);
strictInsertFill(MetaObject metaObject, String fieldName, Supplier fieldVal, Class fieldType)严格模式插入填充在插入操作时严格填充指定字段- MetaObject metaObject: 元对象
- String fieldName: 字段名
- Supplier fieldVal: 字段值提供者
- Class fieldType: 字段类型
metaObjectHandler.strictInsertFill(metaObject, “fieldName”, fieldVal, fieldType);
strictInsertFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills)严格模式插入填充在插入操作时严格填充指定字段- TableInfo tableInfo: 表信息
- MetaObject metaObject: 元对象
- List<StrictFill<?, ?>> strictFills: 填充信息列表
metaObjectHandler.strictInsertFill(tableInfo, metaObject, strictFills);
strictUpdateFill(MetaObject metaObject, String fieldName, Supplier fieldVal, Class fieldType)严格模式更新填充在更新操作时严格填充指定字段- MetaObject metaObject: 元对象
- String fieldName: 字段名
- Supplier fieldVal: 字段值提供者
- Class fieldType: 字段类型
metaObjectHandler.strictUpdateFill(metaObject, “fieldName”, fieldVal, fieldType);
strictUpdateFill(MetaObject metaObject, String fieldName, Class fieldType, E fieldVal)严格模式更新填充在更新操作时严格填充指定字段- MetaObject metaObject: 元对象
- String fieldName: 字段名
- Class fieldType: 字段类型
- E fieldVal: 字段值
metaObjectHandler.strictUpdateFill(metaObject, “fieldName”, fieldType, fieldVal);
strictUpdateFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills)严格模式更新填充在更新操作时严格填充指定字段- TableInfo tableInfo: 表信息
- MetaObject metaObject: 元对象
- List<StrictFill<?, ?>> strictFills: 填充信息列表
metaObjectHandler.strictUpdateFill(tableInfo, metaObject, strictFills);
strictFill(boolean insertFill, TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills)严格填充策略根据表信息和元对象进行严格的插入或更新填充- boolean insertFill: 插入/更新标志
- TableInfo tableInfo: 表信息
- MetaObject metaObject: 元对象
- List<StrictFill<?, ?>> strictFills: 填充信息列表
metaObjectHandler.strictFill(insertFill, tableInfo, metaObject, strictFills);
fillStrategy(MetaObject metaObject, String fieldName, Object fieldVal)填充策略根据策略填充字段值,默认是字段值为null时才填充- MetaObject metaObject: 元对象
- String fieldName: 字段名
- Object fieldVal: 字段值
metaObjectHandler.fillStrategy(metaObject, “fieldName”, fieldVal);
strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal)严格模式填充策略严格填充字段值,只有在当前字段值为null时才填充- MetaObject metaObject: 元对象
- String fieldName: 字段名
- Supplier<?> fieldVal: 字段值提供者
metaObjectHandler.strictFillStrategy(metaObject, “fieldName”, fieldVal);

方法包括检查是否开启填充、具体的填充实现、字段值的设置和获取策略

2. Demo架构

具体的示例如下:

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        System.out.println("start insert fill ....");
        // 使用 strictInsertFill 方法插入时间字段
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        System.out.println("start update fill ....");
        // 使用 strictUpdateFill 方法更新时间字段
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

常用的填充方法如下:

  • strictInsertFill: 严格模式下的插入填充,只有在字段为 null 时才会填充,避免覆盖已有值
  • strictUpdateFill: 严格模式下的更新填充,只有在字段为 null 时才会填充,避免覆盖已有值
  • fillStrategy::指定填充策略,可选值包括 NOT_NULL(仅当字段为 null 时填充)和 DEFAULT(无条件填充)

对应的实体类如下:

@TableName("user")
public class User {

    @TableId(type = IdType.AUTO)
    private Long id;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    // getters and setters
}

上述注解的说明如下(源码说明):

public enum FieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入时填充字段
     */
    INSERT,
    /**
     * 更新时填充字段
     */
    UPDATE,
    /**
     * 插入和更新时填充字段
     */
    INSERT_UPDATE
}

对应还需要增加一个配置类:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MetaObjectHandler metaObjectHandler() {
        return new MyMetaObjectHandler();
    }
}

最终执行插入操作并更新:

User user = new User();
userMapper.insert(user);
// user.getCreateTime() 会被自动填充
userMapper.updateById(user);

此种情况下自动填充属性会失效

  • 字段未使用 @TableField 注解:未使用 @TableField 注解或 fill 属性未设置为 FieldFill.INSERTFieldFill.UPDATE,自动注入将不会生效

  • 未实现 MetaObjectHandler 接口:MyBatis-Plus 将不知道如何处理自动填充逻辑。

  • 未在配置类中注册 MetaObjectHandler:即使实现了 MetaObjectHandler 接口,如果未在 Spring 配置类中进行注册,自动填充也不会生效

3. 全局字段填充(实战)

一些开源项目经常会让我们在创建表的过程中需要保留一些字段,究其原因是属性的自动填充

对于项目中

public class DefaultDBFieldHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
    
        if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();

            LocalDateTime current = LocalDateTime.now();
            // 创建时间为空,则以当前时间为插入时间
            if (Objects.isNull(baseDO.getCreateTime())) {
                baseDO.setCreateTime(current);
            }
            // 更新时间为空,则以当前时间为更新时间
            if (Objects.isNull(baseDO.getUpdateTime())) {
                baseDO.setUpdateTime(current);
            }
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // 更新时间为空,则以当前时间为更新时间
        Object modifyTime = getFieldValByName("updateTime", metaObject);
        if (Objects.isNull(modifyTime)) {
            setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
        }
    }
}

其中BaseDO是我的一些固有属性类:

@Data
@JsonIgnoreProperties(value = "transMap") // 避免 Jackson 在 Spring Cache 反序列化报错
public abstract class BaseDO implements Serializable, TransPojo {

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * 最后更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * 创建者,目前使用 SysUser 的 id 编号
     *
     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
     */
    @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
    private String creator;
    /**
     * 更新者,目前使用 SysUser 的 id 编号
     *
     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
     */
    @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
    private String updater;
    /**
     * 是否删除
     */
    @TableLogic
    private Boolean deleted;

}

同步增加其配置类:

public class MybatisAutoConfiguration {

    @Bean
    public MetaObjectHandler defaultMetaObjectHandler(){
        return new DefaultDBFieldHandler(); // 自动填充参数类
    }
}

对应正式的功能如下:

@Service
@Validated
public class GoodsStoragePlanServiceImpl implements GoodsStoragePlanService {

    @Resource
    private GoodsStoragePlanMapper goodsStoragePlanMapper;

    @Resource
    private AdminUserService userService;


    @Override
    public Long createGoodsStoragePlan(GoodsStoragePlanSaveReqVO createReqVO) {
        // 插入
        GoodsStoragePlanDO goodsStoragePlan = BeanUtils.toBean(createReqVO, GoodsStoragePlanDO.class);
        goodsStoragePlanMapper.insert(goodsStoragePlan);
    }
}

后续便会自动填充相应的属性

4. 局部字段不填充(实战)

代码和第三章的实战差不多,在上面代码的改进

需求:

上面的创建是增加时间和修改时间都会被自动注入
由于我这个只需要增加时间自动注入,修改时间只有在创建的时候自动注入,创建的时候需要为空

增加临时禁用全局字段的填充机制:引入一个上下文变量来控制是否启用字段填充

  1. 创建一个 ThreadLocal 变量来控制是否启用字段填充:
    ThreadLocal 变量为每个线程提供了独立的变量副本,从而保证了线程安全
    每个线程可以独立地访问其 ThreadLocal 变量副本,而不会影响其他线程
/**
 * FieldFillContext 类用于控制全局字段填充机制。
 * 使用 ThreadLocal 变量来保证线程安全,实现线程独立的变量副本。
 */
public class FieldFillContext {

    /**
     * ThreadLocal 变量,用于存储每个线程是否启用字段填充的状态。
     * 初始值为 true,即默认启用字段填充。
     */
    private static final ThreadLocal<Boolean> ENABLE_FIELD_FILL = ThreadLocal.withInitial(() -> true);

    /**
     * 设置当前线程是否启用字段填充。
     *
     * @param enable 如果为 true,则启用字段填充;如果为 false,则禁用字段填充。
     */
    public static void setEnableFieldFill(boolean enable) {
        ENABLE_FIELD_FILL.set(enable);
    }

    /**
     * 获取当前线程是否启用字段填充的状态。
     *
     * @return 如果启用字段填充,则返回 true;否则返回 false。
     */
    public static boolean isEnableFieldFill() {
        return ENABLE_FIELD_FILL.get();
    }

    /**
     * 清除当前线程的 ThreadLocal 变量,防止内存泄漏。
     */
    public static void clear() {
        ENABLE_FIELD_FILL.remove();
    }
}
  1. 修改 MetaObjectHandler 使用上下文变量
    在 MetaObjectHandler 中检查上下文变量的值,决定是否进行字段填充:
public class DefaultDBFieldHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        if (!FieldFillContext.isEnableFieldFill()) {
            return;
        }
        
        /**
        * 以下没变动
        */ 
        if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();

            LocalDateTime current = LocalDateTime.now();
            // 创建时间为空,则以当前时间为插入时间
            if (Objects.isNull(baseDO.getCreateTime())) {
                baseDO.setCreateTime(current);
            }
            // 更新时间为空,则以当前时间为更新时间
            if (Objects.isNull(baseDO.getUpdateTime())) {
                baseDO.setUpdateTime(current);
            }
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // 更新时间为空,则以当前时间为更新时间
        Object modifyTime = getFieldValByName("updateTime", metaObject);
        if (Objects.isNull(modifyTime)) {
            setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
        }
    }
}

对应功能如下:

@Service
@Validated
public class GoodsStoragePlanServiceImpl implements GoodsStoragePlanService {

    @Resource
    private GoodsStoragePlanMapper goodsStoragePlanMapper;

    @Resource
    private AdminUserService userService;


    @Override
    public Long createGoodsStoragePlan(GoodsStoragePlanSaveReqVO createReqVO) {
        // 插入
        GoodsStoragePlanDO goodsStoragePlan = BeanUtils.toBean(createReqVO, GoodsStoragePlanDO.class);
        
        goodsStoragePlan.setCreateTime(LocalDateTime.now());
        goodsStoragePlan.setUpdateTime(null);
        
        // 禁用全局字段填充
        FieldFillContext.setEnableFieldFill(false);
        try {
            goodsStoragePlanMapper.insert(goodsStoragePlan);
        } finally {
            // 恢复全局字段填充
            FieldFillContext.clear();
        }

        // 返回
        return goodsStoragePlan.getId();
    }
}
;