MyBatisPlus 字段填充原理及使用

2,577 阅读6分钟

MyBatisPlus 字段填充原理及使用

前言

在业务中操作中,需要记录操作时间、上下文中的标识信息等公共套用字段,为了简化代码逻辑,可以考虑使用MyBatisPlus 的自动填充功能。

出于对开源组件的好奇心,对MyBatisPlus 这个拓展功能的源码进行了断点跟踪。对该功能的源码阅读分析的进行分享。希望对大家有所帮助。第一次尝试写些源码分析分享,不足之处欢迎指正。

官网的使用指导

public class User {
​
    // 注意!这里需要标记为填充字段
    @TableField(.. fill = FieldFill.INSERT)
    private String fillField;
​
    ....
}
​
  • 自定义实现类 MyMetaObjectHandler

    @Slf4j
    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {
    ​
        @Override
        public void insertFill(MetaObject metaObject) {
            log.info("start insert fill ....");
            this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); //起始版本 3.3.0(推荐使用)
            // 或者
            this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); //起始版本 3.3.3(推荐)
            // 或者
            this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
        }
    ​
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("start update fill ....");
            this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本3.3.0(推荐)
            // 或者
            this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本3.3.3(推荐)
            // 或者
            this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
        }
    }
    ​
    

源码分析

思路

浏览了一下mybatis-plush-core这个jar,发现mybatisPlus有个MybatisParameterHandler。从语义上,MybatisParameterHandler应该就是字段字段填充处理发生的地方。

image-20220527093107806.png

根据官方指导,需要实现MetaObjectHandler这个接口。所以,我安装了官方示例项目,在MyMetaObjectHandler打断点跟踪,发现在执行insertFill或updateFill之前的堆栈存在一个MybatisParameterHandler。MybatisParameterHandler是ParameterHandler的实现类。那么,就可以重点分析MybatisParameterHandler做了哪些事情。

updateFill最近的调用.png

mybatisPlus执行curd时对参数处理的过程

自动填充,是一个对方法入参补充操作的过程,主要是对mybatis调用过程中的parameter进行操作。以update操作举例。

任何框架的源码阅读,总是会受到各种枝叶细节影响,先砍去枝叶看主干,再找自己关注的细节。最后梳理出mybatisPlus对参数的预处理过程。mybatisPlus对参数操作过程如下图。

mybatisPlus参数填充过程分析.png

简单描述一下update 方法的执行过程中对入参的处理:

  1. MyBatis 接收到 update 请求后会先找到 CachingExecutor 缓存执行器查询是否需要刷新缓存,然后找到BaseExecutor 执行 update 方法。时序图步骤1。
  2. 具体的执行器(如SimpleExecutor)会根据Configuration 对象调用 newStatementHandler 方法,由RoutingStatementHandler决定创建并返回对应的statementHandler。这里具体返回的是PreparedStatementHandler。时序图步骤2、3。
  3. 创建PreparedStatementHandler对象时,会执行父类BaseStatementHandler的构造方法。这里会创建MybatisParameterHandler对象赋值给PreparedStatementHandler的ParameterHandler属性。ParameterHandler是参数处理器,也就是说,MybatisPlus对执行update操作时的参数进行介入的时机发生在个部分。时序图步骤4。
  4. 时序图5、6、7为mybatisPlus自动填充功能的核心部分后面会进一步分析这部分具体做了哪些事情。
  5. SimpleExecutor 拿到PreparedStatementHandler后,执行prepareStatement方法对update方法参数进行处理,最终调用的是MybatisParameterHandler的setParameters给参数进行赋值。步骤8、9、10。

MybatisParameterHandler分析

大致了mybatisPlus执行curd时对参数处理的过程之后,重点分析一下MybatisParameterHandler。

MybatisParameterHandler对参数的预处理,着重看processParameter这个方法

    public Object processParameter(Object parameter) {
        /* 只处理插入或更新操作 */
        if (parameter != null
            && (SqlCommandType.INSERT == this.sqlCommandType || SqlCommandType.UPDATE == this.sqlCommandType)) {
            //检查 parameterObject
            if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
                // 如果入参为简单的类型如java.lang、Date、Math等,就不需要做处理
                return parameter;
            }
            //getParameters 如果入参为Collection、map、或多个入参,则统一处理为Collection
            Collection<Object> parameters = getParameters(parameter);
            //调用process方法,对入参进行填充处理。
            if (null != parameters) {
                parameters.forEach(this::process);
            } else {
                //单个对象入参
                process(parameter);
            }
        }
        return parameter;
    }
    //这部分代码是判断哪些入参符合自动填充字段的条件,并对入参对象进行字段填充
    private void process(Object parameter) {
        //通过parameter找到TableInfo
        if (parameter != null) {
            TableInfo tableInfo = null;
            Object entity = parameter;
            if (parameter instanceof Map) {
                Map<?, ?> map = (Map)parameter;
                //et 是mybatisPlus BaseMapper.class 这个类里面用来定义对象入参的@Param的key值。属于mybatisPlus的阅读
                if (map.containsKey("et")) {
                    Object et = map.get("et");
                    if (et != null) {
                        entity = et;
                        //通过class信息找TableInfo
                        tableInfo = TableInfoHelper.getTableInfo(et.getClass());
                    }
                }
            } else {
                //通过class信息找TableInfo
                tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
            }
            //这里必须拿到tableInfo,否则不知道是否需要填充字段
            if (tableInfo != null) {
                MetaObject metaObject = this.configuration.newMetaObject(entity);
                if (SqlCommandType.INSERT == this.sqlCommandType) {
                    this.populateKeys(tableInfo, metaObject, entity);
                    this.insertFill(metaObject, tableInfo);
                } else {
                    this.updateFill(metaObject, tableInfo);
                }
            }
        }     
    }
    protected void insertFill(MetaObject metaObject, TableInfo tableInfo) {
        GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
            //判断更新时是否需要填充字段。fill = FieldFill.INSERT_UPDATE或FieldFill.INSERT 有效
            //只有对象中有一个字段
            if (metaObjectHandler.openInsertFill() && tableInfo.isWithInsertFill()) {
                metaObjectHandler.insertFill(metaObject);
            }
        });
    }
​
    protected void updateFill(MetaObject metaObject, TableInfo tableInfo) {
        GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
            //判断更新时是否需要填充字段。fill = FieldFill.INSERT_UPDATE或FieldFill.UPDAT 有效
            if (metaObjectHandler.openUpdateFill() && tableInfo.isWithUpdateFill()) {
                metaObjectHandler.updateFill(metaObject);
            }
        });
    }   
public class TableInfoHelper {
    ......
    private static final Map<Class<?>, TableInfo> TABLE_INFO_CACHE = new ConcurrentHashMap();
    public static TableInfo getTableInfo(Class<?> clazz) {
        if (clazz != null && !clazz.isPrimitive() && !SimpleTypeRegistry.isSimpleType(clazz) && !clazz.isInterface()) {
            Class<?> targetClass = ClassUtils.getUserClass(clazz);
            TableInfo tableInfo = (TableInfo)TABLE_INFO_CACHE.get(targetClass);
            if (null != tableInfo) {
                return tableInfo;
            } else {
                for(Class currentClass = clazz; 
                    null == tableInfo && Object.class != currentClass; 
                    tableInfo = (TableInfo)TABLE_INFO_CACHE.get(ClassUtils.getUserClass(currentClass))) {
                    currentClass = currentClass.getSuperclass();
                }
​
                if (tableInfo != null) {
                    TABLE_INFO_CACHE.put(targetClass, tableInfo);
                }
​
                return tableInfo;
            }
        } else {
            return null;
        }
    }
    ......    
}

mybatisPlus,是通过入参对象的类信息找到对于的TableInfo,判断是否需要在insert或者update时做。所以入参的对象必须是带有@TableName。

满足以上条件后由MetaObjectHandler的实现类对需要自动填充的字段进行填充。填充字段的核心逻辑在以下代码片段。

public interface MetaObjectHandler {
    ......
    default MetaObjectHandler strictFill(boolean insertFill, TableInfo tableInfo, MetaObject metaObject
                                         , List<StrictFill<?, ?>> strictFills) 
    {
        if (insertFill && tableInfo.isWithInsertFill() || !insertFill && tableInfo.isWithUpdateFill()) {
            strictFills.forEach((i) -> {
                String fieldName = i.getFieldName();
                Class<?> fieldType = i.getFieldType();
                //根据TableFieldInfo的信息过滤出执行insert或update是需要填充的字段,进行填充。
                //段必须有 @TableField(fill = FieldFill.INSERT_UPDATE)等注解
                tableInfo.getFieldList().stream().filter((j) -> {
                    return j.getProperty().equals(fieldName) 
                        && fieldType.equals(j.getPropertyType()) 
                        && (insertFill && j.isWithInsertFill() || !insertFill && j.isWithUpdateFill());
                }).findFirst().ifPresent((j) -> {
                    this.strictFillStrategy(metaObject, fieldName, i.getFieldVal());
                });
            });
        }
​
        return this;
    }        
}

了解了原理之后,总结的使用注意项

1、数据库表对应映射的对象,必须有@TableName(value = "表名");

2、需要填充的字段必须要求@TableField注解,并且fill属性根据需要选对应的枚举值(FieldFill.INSERT、FieldFill.UPDATE、FieldFill.INSERT_UPDATE)

3、支持自动填充的mapper方法定义方式

    void update1(User user);
    void update2(@Param("et") User user);

4、不支持自动填充的mapper方法定义方式

void update1(@Param("user") User user);
void update2(UserVo userVo);//UserVo不含@TableName注解
void updateUser(@Param("id") Long id,@Param("name") String name);