MyBatis-Plus的BaseMapper实现原理

12,709 阅读5分钟

Mybatis-plus为我们提供了一些通用mapper方法,比如insert,update,selectById等等,我们通过让自己的mapper继承BaseMapper这个类就可以不用自己写sql就能直接调用一些基础的sql方法。

public interface BaseMapper<T> extends Mapper<T> {}

但是在使用过程中还是发现这里面提供的方法有点少,当我们想要添加自己的通用sql方法的时候,可以通过官方文档描述的 Sql 注入器 来实现。比我们我们自己定义一个saveBatch方法,用来批量的插入数据。

BaseMapper自定义扩展

mybatis-plus提供了ISqlInjector接口,以及AbstractSqlInjector抽象类。我们通过实现该接口,或者继承抽象类的方式注入我们自已定义的SQL逻辑,然后继承BaseMapper添加我们需要的方法就可以添加自定义的mapper方法。

在这2个接口之外,mybatis-plus其实为我们提供了一个默认实现:DefaultSqlInjector,这里面已经包含了一些mybatis-plus已经封装好的BaseMapper里面的方法,我们想要扩展的话,可以直接继承这个类来进行扩展添加我们的方法。

这里我们希望在BaseMapper之外添加一个saveBatch方法,用来可以批量的插入数据:

  1. 实现DefaultSqlInjector类,我们可以看到需要实现getMethodList方法,这个方法参数是mapper接口的class类,返回值是一个List<AbstractMethod>。所以我们自定义的方法是需要实现AbstractMethod。可以参考mybatis-plus中已经实现了一些AbstractMethod方法,我们仿造写一个SaveBatch类。
public class CustomSqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
      	// 父类的list已经包含了BaseMapper的基础方法。
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        // 添加我们需要增加的自定义方法。
        methodList.add(new SaveBatch());
        return methodList;
    }
}
  1. 实现SaveBatch类的逻辑(这是官方的samples)。我们可以看到,这里的逻辑主要就是为了生成MappedStatement对象
public class SaveBatch extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        final String sql = "<script>insert into %s %s values %s</script>";
        final String fieldSql = prepareFieldSql(tableInfo);
        final String valueSql = prepareValuesSqlForMysqlBatch(tableInfo);
        final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql);
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, "saveBatch", sqlSource, new NoKeyGenerator(), null, null);
    }


    private String prepareFieldSql(TableInfo tableInfo) {
        StringBuilder fieldSql = new StringBuilder();
        fieldSql.append(tableInfo.getKeyColumn()).append(",");
        tableInfo.getFieldList().forEach(x -> {
            fieldSql.append(x.getColumn()).append(",");
        });
        fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
        fieldSql.insert(0, "(");
        fieldSql.append(")");
        return fieldSql.toString();
    }


    private String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) {
        final StringBuilder valueSql = new StringBuilder();
        valueSql.append("<foreach collection=\"list\" item=\"item\" index=\"index\" open=\"(\" separator=\"),(\" close=\")\">");
        valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
        tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
        valueSql.delete(valueSql.length() - 1, valueSql.length());
        valueSql.append("</foreach>");
        return valueSql.toString();
    }

}
  1. 最后我们需要将我们的Injector注入Spring容器中,替换默认的Injector。
@Bean
public CustomSqlInjector myLogicSqlInjector() {
    return new CustomSqlInjector();
}
  1. 验证:
public interface TB3Mapper extends MyBaseMapper<Tb3> {
}

@Test
public void test() {
    List<Tb3> tb3s = Arrays.asList(Tb3.getInstance(), Tb3.getInstance());
    tb3Mapper.saveBatch(tb3s);
}
// output log
==>  Preparing: insert into tb3 (id,f1,f2,f3) values ( ?,?,?,? ),( ?,?,?,? )
==> Parameters: 38(Integer), 62(Integer), -1546785812(Integer), -16950756(Integer), 24(Integer), 17(Integer), -1871764773(Integer), 169785869(Integer)
<==    Updates: 2

原理解析

首先简单说下mybatis-plus,我只是简单翻了下源码,mybatis-plus的工作原理就是:全面再次代理了mybatis的一些东西。比如自动配置转用了MybatisPlusAutoConfiguration,SqlSessionFactoryBean转用了MybatisSqlSessionFactoryBean等等,这些mybatis的核心部件,全部都被mybatis-plus给替换了,用成了它自己的,然后它就在自己的里面定制了自己的逻辑。

我只分析下BaseMapper的实现原理,当初在文档中没有看到这块,自己手写了一版自定义的逻辑,跟踪了这块代码。在这之前还是简单阐述下mybatis的一些核心的原理,如果没有看过mybatis源码的话,知道这些也应该能看懂。

mybatis的整体逻辑可以分为2块

  1. 配置文件解析:这过程包括解析我们config的配置,以及mapper.xml文件。最终配置都会被解析到一个Configuration对象里面,后面的每个SqlSession也都会包含一个该Configuration对象实例的引用。这个Configuration里面最重要的有2个东西:

image-20210830223002642.png

  • mappedStatements:存放mapper对应的sql信息
  • mybatisMapperRegistry.knownMappers:存放mapper接口对应的代理类

这2个东西贯穿了mybatis的接口到sql的执行逻辑。

  1. 接口的调用:我们接口调用的其实是代理的包装类,这个类也是上图mybatisMapperRegistry.knownMappers里面展示的MybatisMapperProxyFactory(mybatis是MapperProxyFactory)的getObject返回的代理MybatisMapperProxy对象。这个代理类里面的主要逻辑就是拿着该类的全限定类名,指定某个方法的时候,去Configuration的mappedStatements里面去找到对应的sql。

所以知道了mybatis的大概逻辑了,我们可以猜到:在Configuration加载的时候,一定有地方将我们BaseMapper的默认方法对应的SQL的信息给装载到mappedStatements这个map里面去。我们就需要去跟踪,在哪里我们将这些默认的基础方法的MappedStatement对象进行构建,并插入到configuration中的。

debug跟踪可以发现:

第一步,肯定是自动配置要加载SqlSessionFactory,这个方法主要是构建MybatisSqlSessionFactoryBean对象,然后调用getObject方法,我们跟进MybatisSqlSessionFactoryBean.getObject()

@Override
public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
        afterPropertiesSet();
    }
    return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
        "Property 'configuration' and 'configLocation' can not specified with together");
  	// 这里才是开始构建SqlSessionFactory的
    this.sqlSessionFactory = buildSqlSessionFactory();
}

可以看到,最终会执行到buildSqlSessionFactory()。这块方法的主要逻辑就是解析XML配置来创建Configuration对象。我们可以在最下面发现解析我们mapper.xml文件的逻辑:

if (this.mapperLocations != null) {
    if (this.mapperLocations.length == 0) {
        LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
    } else {
        for (Resource mapperLocation : this.mapperLocations) {
            if (mapperLocation == null) {
                continue;
            }
            try {
              	// 对每一个mapper.xml文件进行解析
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                    targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
                xmlMapperBuilder.parse();
            } catch (Exception e) {
                throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
            } finally {
                ErrorContext.instance().reset();
            }
            LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
        }
    }
} else {
    LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}

初步猜测:sql信息就是在这里面被装载进去的,重点看看xmlMapperBuilder.parse();

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    // debug发现,Configuration中mappedStatements在执行该方法之后,mapper方法数量就变多了。
    bindMapperForNamespace();
  }
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

bindMapperForNamespace里面,是在执行configuration.addMapper(boundType);之后方法变多的。这个方法最终调用的是MybatisMapperRegistry.addMapper(),这个方法里面最终会转去调用MybatisMapperAnnotationBuilder.parse()方法,将mapper的方法加入到mappedStatements中。

@Override
public void parse() {
  	......
        try {
            // https://github.com/baomidou/mybatis-plus/issues/3038
            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
              	// 执行该步骤之后,新增了mappestatment
                parserInjector();
            }
        } catch (IncompleteElementException e) {
            configuration.addIncompleteMethod(new InjectorResolver(this));
        }
    }
    parsePendingMethods();
}

parserInjector方法如下:

void parserInjector() {
    GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
//GlobalConfigUtils.getSqlInjector
public static ISqlInjector getSqlInjector(Configuration configuration) {
    return getGlobalConfig(configuration).getSqlInjector();
}
//getSqlInjector()
private ISqlInjector sqlInjector = new DefaultSqlInjector();
//MybatisPlusAutoConfiguration.sqlSessionFactory#sqlInjector
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);

我们可以看到,通过一连串的方法拿到ISqlInjector实现类。默认是DefaultSqlInjector,但是如果Spring中被手动注入了该实现类的话,就会在自动配置的时候,修改为我们自定义的SqlInjector。

image-20210830230401071.png

这里就会转到我们自定义的逻辑了,但是我们这里CustomSqlInjector是extends DefaultSqlInjector的,所以逻辑还是在DefaultSqlInjector里面。

//DefaultSqlInjector
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
    return Stream.of(
        new Insert(),
        new Delete(),
      //....
    ).collect(toList());
}
//AbstractSqlInjector
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
    Class<?> modelClass = extractModelClass(mapperClass);
    if (modelClass != null) {
        String className = mapperClass.toString();
        Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
        if (!mapperRegistryCache.contains(className)) {
          	// 可以看到这里拿取我们CustomSqlInjector返回的AbstractMethod list,然后循环调用inject
            List<AbstractMethod> methodList = this.getMethodList(mapperClass);
            if (CollectionUtils.isNotEmpty(methodList)) {
                TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                // 循环注入自定义方法
                methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
            } else {
                logger.debug(mapperClass.toString() + ", No effective injection method was found.");
            }
            mapperRegistryCache.add(className);
        }
    }
}
//AbstractMethod
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    this.configuration = builderAssistant.getConfiguration();
    this.builderAssistant = builderAssistant;
    this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
    /* 注入自定义方法 */
    injectMappedStatement(mapperClass, modelClass, tableInfo);
}

这里就很清楚了,这里面一系列的模板方法模式的类,预留了钩子让子类去实现。

DefaultSqlInjector这个类只是为了提供哪些方法需要被注入到mappedStatements中,这个list将被抽象类AbstractSqlInjector钩子调用。

AbstractSqlInjector主要就是循环getMethodList返回的AbstractMethod组成的List,然后调用inject方法。

AbstractMethodinject也就是我们自己定义的逻辑。

SaveBatch在构建好一个MappedStatement对象需要的元素后,调用addInsertMappedStatement将插入到Configuration的mappedStatements中。

分析完毕。

番外

mybatis-plus除了BaseMapper之外,还有一些公共的方法,是放在一个 ServiceImpl的类中的,很多人在service层继承这个类来获取这些功能,我一直很不喜欢这种方式:

  • 在Service层继承这个东西,感觉像是把Dao的功能迁移到了Service层面了,层次结构有点不舒服(当然实际使用没什么影响)。
  • 这个ServiceImpl里面很多方法被强制加了是事务注解,我们都无法改变!这个比较糟糕,多数据源的时候这些事务注解会导致数据源切换失败。

我的想法是这些方法能不能再落到BaseMapper层?经过这次分析发现,确实不太合适:BaseMapper里面的基础方法一般都对应这一条SQL,这条SQL是能被完整构建的。

但是在ServiceImpl里面的方法,很多其实都是打包多条SQL然后统一提交进行flush操作的,甚至一写比如saveOrUpdate方法都是执行查询,然后处理后再更新或者插入。这些都不是单个SQL能完成的任务,因此mybatis-plus将这些逻辑只能放在ServiceImpl中。

如果想将这些方法,还是放在BaseMapper中,那么可能就需要去特别的修改MapperProxy代理类了。相较而言,放在ServiceImpl还是不错的选择。