前言

Mybatis-Plus是一个 MyBatis增强工具包,简化 CRUD 操作,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生,号称无侵入,现在开发中比较常用,包括我自己现在的项目中ORM框架除使用JPA就是他了。

我好奇的是他是如何实现单表的CRUD操作的?

不看源码之前,其实我大致能猜一猜:因为他号称零入侵,只做增强,那我们就能简单的理解为他只是在上面做了一层封装类似于装饰器模式,简化了许多繁重的操作。

但是万变不离其宗,他最后应该还是执行MyBatis里Mapper注册MappedStatement这一套,所以他应该是内置了一套CRUD的SQL模板,根据不同的entity来生成对应的语句,然后注册到Mapper中用来执行。

带着猜想,我们具体跟下他的注册流程。

1.MybatisPlusAutoConfiguration

Mybatis-Plus依托于spring,一切都是用的ioc这一套。创建SqlSessionFactory从之前的SqlSessionFactoryBuilder主动创建改成ioc来控制创建。具体我们看一代码:

@Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // TODO 使用 MybatisSqlSessionFactoryBean 而不是 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()));
        }
        //初始化configuration
        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);
        }
        //获得mapper文件
        Resource[] mapperLocations = this.properties.resolveMapperLocations();
        if (!ObjectUtils.isEmpty(mapperLocations)) {
            factory.setMapperLocations(mapperLocations);
        }

        // TODO 对源码做了一定的修改(因为源码适配了老旧的mybatis版本,但我们不需要适配)
        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
        if (!ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
        }
        Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);

        // TODO 自定义枚举包
        if (StringUtils.hasLength(this.properties.getTypeEnumsPackage())) {
            factory.setTypeEnumsPackage(this.properties.getTypeEnumsPackage());
        }
        // TODO 此处必为非 NULL
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
        // TODO 注入填充器
        this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
        // TODO 注入主键生成器
        this.getBeanThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerator(i));
        // TODO 注入sql注入器
        this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
        // TODO 注入ID生成器
        this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
        // TODO 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
        factory.setGlobalConfig(globalConfig);
        return factory.getObject();
    }

代码比较简单,再加上是国人开发的框架,在关键节点上有一定的注释,所以看上去还算是轻松加愉快。这个方法基本上就是MybatisSqlSessionFactoryBean的初始化操作。

我们主要是看Mapper的生成,所以其它的放一旁,所以我们基本最在意的应该是注入sql注入器this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector)

2.ISqlInjector(SQL自动注入器接口)

public interface ISqlInjector {

    /**
     * 检查SQL是否注入(已经注入过不再注入)
     *
     * @param builderAssistant mapper 信息
     * @param mapperClass      mapper 接口的 class 对象
     */
    void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}
public abstract class AbstractSqlInjector implements ISqlInjector {

    private static final Log logger = LogFactory.getLog(AbstractSqlInjector.class);

    @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)) {
                //获得CRUD一系列的操作方法
                List<AbstractMethod> methodList = this.getMethodList(mapperClass);
                if (CollectionUtils.isNotEmpty(methodList)) {
                    //取得对应TableEntity
                    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);
            }
        }
    }
/**
 * SQL 默认注入器
 *
 * @author hubin
 * @since 2018-04-10
 */
public class DefaultSqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        return Stream.of(
            new Insert(),
            new Delete(),
            new DeleteByMap(),
            new DeleteById(),
            new DeleteBatchByIds(),
            new Update(),
            new UpdateById(),
            new SelectById(),
            new SelectBatchByIds(),
            new SelectByMap(),
            new SelectOne(),
            new SelectCount(),
            new SelectMaps(),
            new SelectMapsPage(),
            new SelectObjs(),
            new SelectList(),
            new SelectPage()
        ).collect(toList());
    }
}

ISqlInjector接口只有一个inspectInject方法来提供SQL注入的操作,在AbstractSqlInjector抽象类来提供具体的操作,最终对外的默认实现类是DefaultSqlInjector。

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

看到这,通过上面的注释,先是不是跟我们最开始的猜想已经有点眉目了?

我们简单看下SelectOne操作。

public class SelectOne extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        SqlMethod sqlMethod = SqlMethod.SELECT_ONE;
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(),
            sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
            sqlWhereEntityWrapper(true, tableInfo), sqlComment()), modelClass);
        return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
    }
}

上面就是具体生成MappedStatement的地方,细节就不说了,其实追踪到最后都是一跟之前篇章的分析是一样的。

我们主要是看SqlMethod.SELECT_ONE就是框架中自定义SQL的地方。我们打开SqlMethod就可以看到全部的SQL语句。

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

其实看到这,我们就大概了解了整个单表CRUD生成的方法,其实如果我们想要实现自己的类似的自定义SQL,就可以实现AbstractSqlInjector抽象类。

生成自己的DefaultSqlInjector,然后在仿照框架的写法,实现自己的injectMappedStatement方法,这样就可以了。

3.inspectInject的调用

分析完上面的重头戏,我们正常还是要看下inspectInject在哪被调用的,直接跟跟踪下代码,我们就能轻易的追踪到代码调用的地方,MybatisConfigurationaddMapper的时候会调用。

我们直接跳到调用的地方。

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

而调用addMapper的地方,第一个我们很容易找到,就是在buildSqlSessionFactory里解析mapperLocations的时候。这一块的代码基本上就是之前的xml解析这一套,跟之前的mybatis解析是差不多的,

所以就不累述了。

...
        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 {
                        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 + "'");
                }
            }
...

重头戏到了,xml的解析这一套我们都找到了,那那些没有配置xml的mapper接口呢?他是如何注册的?

4.MybatisPlusAutoConfiguration

其实通过打断点,我们是能找到调用addMapper的地方,就在MapperFactoryBean中的checkDaoConfig方法中。

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

当时就懵逼了,mapper接口是怎么变成MapperFactoryBeanFactoryBean用来spring里用来bean封装这一套我们是理解的,关键是我们的mapper接口在哪进行转换的呢?

首先分析下我们的Mapper接口是怎么被发现的?这么一想,我就立刻想到了在启动类上的@MapperScan(basePackages = {"com.xx.dao"}),这个@MapperScan注解就是扫描对应包下面的mapper进行注册的。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
...

打开我们就发现了MapperScannerRegistrar类,它实现了ImportBeanDefinitionRegistrar接口,在registerBeanDefinitions方法中进行手动注册bean的操作

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

 void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName) {

    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
    builder.addPropertyValue("processPropertyPlaceHolders", true);
    ...
    ...
    builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));

    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

  }
终于找到了根源了: MapperScannerConfigurer。

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

我们看下这个类的注释:BeanDefinitionRegistryPostProcessor从基包开始递归搜索接口,并将其注册为MapperFactoryBean 。 请注意,只有具有至少一种方法的接口才会被注册; 具体的类将被忽略。

从上面这段话,我们就大致知道他的作用,他实现了BeanDefinitionRegistryPostProcessor接口,在postProcessBeanDefinitionRegistry里对所有的package下扫描到的未实例但已注册的bean进行封装处理。具体我们看下代码:

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    scanner.registerFilters();
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

postProcessBeanDefinitionRegistry 方法里注册了一个ClassPathBeanDefinitionScanner,一个扫描器。它通过basePackage, annotationClass或markerInterface注册markerInterface 。 如果指定了annotationClass和/或markerInterface ,则仅搜索指定的类型(将禁用搜索所有接口)。作用很明显了,在我们这的作用就是通过basePackage来扫描包内的所有mapperbeans。

最后一步的scan操作,我们来看下操作。

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

最终再说下所有mapper注入的地方,在ServiceImpl里:

Mybatis3源码笔记(八)小窥MyBatis-plus-LMLPHP

04-23 22:00