<返回更多

彻底搞清mybaties plus实现原理

2022-11-18  今日头条  奋斗的架构师
加入收藏

讨论主题

主要想搞清楚几个问题

  1. MyBatis plus是依赖了mybatis,他们之间的关系是什么?
  2. mybatis plus中“字段自动填充功能”实现源码,包括id自动生成的原理。
  3. mybatis plus 基本的增删改查为什么不用写sql?以及sql注入器的原理。

本篇文章需要理解mybaties的源码为基础,否则看本篇文章会吃力。mybaties源码分析可以看我上一篇文章,如下 彻底看懂springboot mybaties源码流程

mybaties plus是依赖了mybaties,他们之间的关系是什么?

mybatis plus是基于mybatis实现的,下面来具体看看他们直接的关系,以及是怎样依赖的?

首先看mybatis plus自动配置类中的核心方法sqlSessionFactory,如下:

MybatisPlusAutoConfiguration

public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // mybaties plus使用 MybatisSqlSessionFactoryBean ,mybaties使用SqlSessionFactoryBean
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }
        ApplyConfiguration(factory);
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }
        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }
        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        if (this.properties.getTypeAliasesSuperType() != null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }
        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }
        this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);
        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
        if (!ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
        }
        Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);
        // 提供了可定制MybatisSqlSessionFactoryBean的类,如果你想定制它,你可以自定定义SqlSessionFactoryBeanCustomizer类型的类。
        applySqlSessionFactoryBeanCustomizers(factory);

        // mybaties plus定义的全局的配置类,供后续方便使用。
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
        // 从spring容器中获取定义的MetaObjectHandler类型的实例,并设置到globalConfig供后续使用,这个就是
        // 字段自动填充功能的类。
        this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
        // 从spring容器中获取IKeyGenerator类型实例(主要是实现主键生成器),并设置到globalConfig供后续使用
        this.getBeansThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerators(i));
        // 从spring容器中获取ISqlInjector类型实例(Sql注入器),并设置到globalConfig供后续使用
        this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
        //  从spring容器中获取IdentifierGenerator类型实例(主要是实现ID生成器),并设置到globalConfig供后续使用
        this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
        // 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
        factory.setGlobalConfig(globalConfig);
        return factory.getObject();
    }

可以看到mybatis plus使用
MybatisSqlSessionFactoryBean (mybaties中使用的是SqlSessionFactoryBean),然后针对mybatis plus进行了一系列的初始化操作,并把相关的实例都设置到了GlobalConfig中,这块是mybatis plus扩展的初始化位置。

mybaties plus定制了mybaties很多核心类,总结如下:

mybaties plus

mybaties

功能描述

MybatisSqlSessionFactoryBean

SqlSessionFactoryBean

调用buildSqlSessionFactory创建SqlSessionFactory类

MybatisConfiguration

Configuration

用于描述 MyBatis 主配置文件信息,MyBatis 框架在启动时自动配置类中,会加载mapper配置文件,将配置信息转换为 Configuration 对象,然后把该对象传入给sqlSessionFactory供后续使用

MybatisMapperAnnotationBuilder

MapperAnnotationBuilder

解析Mapper方法中用注解方式定义的sql。

MybatisMapperRegistry

MapperRegistry

Mapper注册器,其实就是加入到一个内部数组中。

MybatisParameterHandler

DefaultParameterHandler

用于处理 SQL 中的参数占位符,为参数占位符设置值

上面是mybaties plus扩展mybaties的核心类。

答疑时刻

mybatis plus扩展了mybatis的很多功能,添加了很多实用功能。比如最主要的基于对象的增删改查(不需要写sql),基于雪花算法的ID生成器,字段自动填充功能,逻辑删除功能等。

mybatis plus中“字段自动填充功能”实现源码,包括id自动生成的原理。

字段自动填充功能可以干什么?

在开发过程中表中经常会建创建时间,创建人,更新时间,更新人字段,正常自己维护这几个字段的时候,插入数据的时候需要自己给创建时间创建人赋值。更新数据的时候需要自己给更新时间,更新人字段赋值。

mybaties plus “字段自动填充功能”就是来解决这个问题的,可以在插入数据的时候指定要更新哪些字段,更新数据的时候指定更新哪些字段。用起来还是很方便的。

下面是使用例子:

public class User {
    // 注意!这里需要标记为填充字段
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT)
    private String createUserName;
    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.UPDATE)
    private String updateUserName;
}

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");
        this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); 
        this.strictInsertFill(metaObject, "createUserName", () -> "wanglining", String.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class);
        this.strictInsertFill(metaObject, "updateUserName", () -> "wanglining", String.class)
     }
}

主要两步:

  1. 在对象上标记要填充的字段。
  2. 定义字段填充handler。

经过上面两个步骤,自动填充功能就完成了,当你调用mybaties plus的BaseMapper中提供的insert方法的时候,会给createTime,createUserName两个字段填充你在handler中指定的值。相应的调用BaseMapper提供的update方法的时候,会给updateTime,updateUserName两个字段填充你在handler中指定的值。

注意:一定得是调用BaseMapper中提供的方法,如果你自己再xml中定义sql语句是不会有作用的。

实现原理

下面开始讲解它的实现原理。

在上面提到mybaties plus定制了参数处理器(ParameterHandler),mybaties plus中的实现为MybatisParameterHandler,它的作用是“用于处理 SQL 中的参数占位符,为参数占位符设置值”, mybaties plus就是用在jdbc真正执行sql前,通过MetaObjectHandler给对象相应字段赋值。进而mybaties把对象解析到了sql中进行执行。

MybatisParameterHandler

private void process(Object parameter) {
        if (parameter != null) {
            TableInfo tableInfo = null;
            Object entity = parameter;
            if (parameter instanceof Map) {
                // 处理单参数使用注解标记的时候,尝试提取et来获取实体参数
                Map<?, ?> map = (Map<?, ?>) parameter;
                if (map.containsKey(Constants.ENTITY)) {
                    Object et = map.get(Constants.ENTITY);
                    if (et != null) {
                        entity = et;
                        tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
                    }
                }
            } else {
                tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
            }
            if (tableInfo != null) {
                //到这里就应该转换到实体参数对象了,因为填充和ID处理都是针对实体对象处理的,不用传递原参数对象下去.
                MetaObject metaObject = this.configuration.newMetaObject(entity);
                if (SqlCommandType.INSERT == this.sqlCommandType) {
                    // 这个方法实现了给主键ID赋值的,根据你指定的策略生成id值,并赋值给主键ID。
                    populateKeys(tableInfo, metaObject, entity);
                    // 这里会进一步调用上面定义的handler里面的方法
                    insertFill(metaObject, tableInfo);
                } else {
                    // 这里会进一步调用上面定义的handler里面的方法
                    updateFill(metaObject, tableInfo);
                }
            }
        }
    }
    protected void insertFill(MetaObject metaObject, TableInfo tableInfo) {
        // 这里会先获取到你上面定义的MyMetaObjectHandler。
        GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
            // 判断openInsertFill是否为true,模式的就是返回true
            // isWithInsertFill判断是否为true,这个方法返回的就是TableInfo类中的withInsertFill属性
            // TableInfo就是dao层对象以及字段注解解析出来的一个对应数据库表的对象,其中withInsertFill属性
            // 就是根据类的字段上是否有@TableField(fill = FieldFill.INSERT)注解,如果有那么withInsertFill
            // 最终会是true
            if (metaObjectHandler.openInsertFill() && tableInfo.isWithInsertFill()) {
                // 调用MyMetaObjectHandler的insertFill方法
                metaObjectHandler.insertFill(metaObject);
            }
        });
    }
    // 这个方法就不写注释了,跟上面一样。
    protected void updateFill(MetaObject metaObject, TableInfo tableInfo) {
        GlobalConfigUtils.getMetaObjectHandler(this.configuration).ifPresent(metaObjectHandler -> {
            if (metaObjectHandler.openUpdateFill() && tableInfo.isWithUpdateFill()) {
                metaObjectHandler.updateFill(metaObject);
            }
        });
    }


metaObjectHandler.insertFill会调用上面自己定义的MyMetaObjectHandler的insertFill方法,然后会继续调用strictInsertFill方法。

分析下strictInsertFill方法如下:

MetaObjectHandler

    default MetaObjectHandler strictInsertFill(TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
        return strictFill(true, 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) {
        // 先过滤表上withInsertFill或withUpdateFill属性是否为true,其实就是看dao层对应的类属性上是否有
        // @TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.UPDATE)注解
        if ((insertFill && tableInfo.isWithInsertFill()) || (!insertFill && tableInfo.isWithUpdateFill())) {
            strictFills.forEach(i -> {
                final String fieldName = i.getFieldName();
                final Class<?> fieldType = i.getFieldType();
                tableInfo.getFieldList().stream()
                    // 过滤对象字段,把字段上有@TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.UPDATE)注解
                    // 的字段过滤出来。
                    .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;
    }

上面分析完了MybatisParameterHandler处理字段自动填充流程的核心逻辑,是从它的方法process说起的,那这个方法又是怎么被触发的?怎么和mybaties执行流程对接上的?

下面是mybaties 查询方法完成的调用时序图

 

上面重点关注下MybatisParameterHandler类的方法,主要两步:

  1. 在执行器进行具体jdbc操作前,先初始化了StatementHandler,StatementHandler里面会创建MybatisParameterHandler,在MybatisParameterHandler的构造方法中最终会调用process方法,进而为对象进行字段填充操作。
  2. 在prepareStatement阶段,jdbc执行prepare操作完成后会返回一个Statement,StatementHandler会调用MybatisParameterHandler的setParameters方法,解析dao层对象属性,并把参数值设置到prepare返回的Statement中。

经过上面两步ParameterHandler的任务就完成了。

mybatis plus 基本的增删改查为什么不用写sql?以及sql注入器的原理。

mybatis plus通过定义sql注入器实现了此功能。核心类

DefaultSqlInjector

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
            // 注入BaseMapper的insert方法
            .add(new Insert())
            // 注入BaseMapper的delete方法
            .add(new Delete())
            .add(new DeleteByMap())
            .add(new Update())
            .add(new SelectByMap())
            .add(new SelectCount())
            .add(new SelectMaps())
            .add(new SelectMapsPage())
            .add(new SelectObjs())
            // 注入BaseMapper的selectList方法
            .add(new SelectList())
            .add(new SelectPage());
        if (tableInfo.havePK()) {
            builder.add(new DeleteById())
                .add(new DeleteBatchByIds())
                .add(new UpdateById())
                .add(new SelectById())
                .add(new SelectBatchByIds());
        } else {
            logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
                tableInfo.getEntityType()));
        }
        return builder.build().collect(toList());
    }

这里以insert举例看下Insert内部实现。

Insert

/**
 * 插入一条数据(选择字段插入)
 *
 * @author hubin
 * @since 2018-04-06
 */
public class Insert extends AbstractMethod {

    public Insert() {
        super(SqlMethod.INSERT_ONE.getMethod());
    }

    /**
     * @param name 方法名
     * @since 3.5.0
     */
    public Insert(String name) {
        super(name);
    }

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        // INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>nINSERT INTO %s %s VALUES %sn</script>")
        // INSERT_ONE枚举里面定义了sql脚本,以及BaseMapper中对应方法名
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        // 通过dao类对应表信息,获取表中的列并且拼“接插入sql”的列部分
        String columnScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlColumnMaybeIf(null),
            LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
        // 通过dao类对应表信息,获取表中的列并且拼接“插入sql”的值部分
        String valuesScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlPropertyMaybeIf(null),
            LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
        String keyProperty = null;
        String keyColumn = null;
        // 表包含主键处理逻辑,如果不包含主键当普通字段处理
        // 获取主键部分的属性和对应列信息
        if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
            if (tableInfo.getIdType() == IdType.AUTO) {
                /* 自增主键 */
                keyGenerator = Jdbc3KeyGenerator.INSTANCE;
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            } else if (null != tableInfo.getKeySequence()) {
                keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant);
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            }
        }
        // 通过上面获取的插入sql脚本,以及列和值部分,拼接成最终的sql脚本字符串。
        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        // 通过上面的信息生成对应的MapperStatement,并且注册到mybaties容器中。供后续真正调用BaseMapper对应方法
        // 的时候再取出来使用。
        return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
    }

通过上面的代码,可以想象到mybaties plus初始化过程中肯定会调用到Insert的injectMappedStatement方法,把它对应的MapperStatement注入到mybaties容器中,然后后续通过动态代理调用BaseMapper相关方法的时候就可以根据MapperStatement对应的信息去执行对应的sql了。

现在分析下injectMappedStatement方法是怎么被调用到的?

调用时序图如下:

 

这里关注下Insert的injectMappedStatement调用流程。

这里再重点分析MybatisMapperRegistry的addMapper代码,因为它的信息量比较大。如下:

MybatisMapperRegistry

    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // TODO 如果之前注入 直接返回
                return;
                // TODO 这里就不抛异常了
//                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                // 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
                // 对于mybaties plus用的MybatisMapperProxyFactory,mybaties用MapperProxyFactory
                // 这句代码很重要,把动态代理工厂加入到了knownMappers中,这里的type是你定义的Mapper类。
                // 下面的getMapper会通过MybatisMapperProxyFactory生成对应mapper的动态代理,进而执行mapper
                // 的各个方法。
                knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                // TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
                // 解析Mapper定义的方法中用注解方式定义的sql。这里最终会在你定义的Mapper类中添加mybaties plus
                // 为你注入的Insert,Delete ,Update,Select等方法。其实就是把对应的MapperStatement注入到了Mybaties中。
                MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
    @Override
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // TODO 这里换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
        // fix https://github.com/baomidou/mybatis-plus/issues/4247
        MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            // 上面addMapper会在knownMappers添加对应的Factory
            mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.entrySet().stream()
                .filter(t -> t.getKey().getName().equals(type.getName())).findFirst().map(Map.Entry::getValue)
                .orElseThrow(() -> new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry."));
        }
        try {
            // 生成mapper对应的动态代理对象
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }

答疑时刻

mybatis plus定义了增删改查对应的sql注入器,在解析注册mapper的时候,会把相应的方法作为MapperStatement注入到mybaties中。看起来没有定义对应的xml文件,其实是mybaties plus用sql注入器的方式,在代码中默认都提供了对应的sql了。

后面当你调用Mapper对应方法的时候,会通过对应mapper的动态代理获取到方法对应的MapperStatement,进而再通过执行器进行一系列的处理,最终执行该方法对应的sql。


通过上面的原理分析,也可以注入自己的sql,比如批量插入方法,mybaties plus默认没有提供该方法,你可以自己定义一个BaseMapper然后扩展出这个批量插入的方法,下一篇文章再讲解吧。

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>