前言

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。

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

我们简单看下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语句。

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

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

3.inspectInject的调用

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

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

而调用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方法中。

当时就懵逼了,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的操作

 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。

我们看下这个类的注释: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操作,我们来看下操作。

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

Mybatis3源码笔记(八)小窥MyBatis-plus的更多相关文章

  1. Tomcat8源码笔记(八)明白Tomcat怎么部署webapps下项目

    以前没想过这么个问题:Tomcat怎么处理webapps下项目,并且我访问浏览器ip: port/项目名/请求路径,以SSM为例,Tomcat怎么就能将请求找到项目呢,项目还是个文件夹类型的? Tom ...

  2. Mybatis3源码笔记(一)环境搭建

    1. 源码下载 地址:https://github.com/mybatis/mybatis-3.git. 国内访问有时确实有点慢,像我就直接先fork.然后从git上同步到国内的gitte上,然后在i ...

  3. Mybatis3源码笔记(六)SqlSession执行过程

    前几篇大致分析了初始化的过程,今天打算走一个SqlSession具体执行过程. @Test void shouldSelectAllAuthors() { try (SqlSession sessio ...

  4. Mybatis3源码笔记(七)Plugin

    1.Mybatis3的插件其实主要是用到了责任链和动态代理两种模式相结合而生成的.下面我们看一个例子,在执行所有update操作时,执行一个小小的测试输出. @Intercepts({@Signatu ...

  5. Mybatis3源码笔记(四)Configuration(续)

    1.pluginElement(root.evalNode("plugins")) 解析plugins节点(注册interceptorChain里记录对应的拦截器) private ...

  6. Mybatis3源码笔记(三)Configuration

    1. XMLConfigBuilder 上一篇大致介绍了SqlSession的生成.在DefaultSqlSessionFactory的构造函数中就提到了Configuration这个对象.现在我们来 ...

  7. Mybatis3源码笔记(二)SqlSession

    1. 核心层次 2. SqlSession 先从顶层的SqlSession接口开始说起.SqlSession是MyBatis提供的面向用户的API,表示和数据库的会话对象,用于完成对数据库的一系列CR ...

  8. Mybatis3源码笔记(五)mapperElement

    1.四种解析mapper方式 : package,resource,url,class. <mappers> <mapper resource="org/apache/ib ...

  9. Tomcat8源码笔记(三)Catalina加载过程

    之前介绍过 Catalina加载过程是Bootstrap的load调用的  Tomcat8源码笔记(二)Bootstrap启动 按照Catalina的load过程,大致如下: 接下来一步步分析加载过程 ...

随机推荐

  1. three.js cannon.js物理引擎之ConvexPolyhedron多边形

    年后第一天上班,郭先生来说一说cannon.js的ConvexPolyhedron(多边形),cannon.js是一个物理引擎,内部通过连续的计算得到各个时间点的数据的状态,three.js的模型可以 ...

  2. 字符串拼接出现null的问题

    最近在开发的过程中遇到这样的问题,原因是在做一个需求的时候,要求将解密的号码和前缀进行拼接.一开始在这个拼接的过程中,没有考虑到数据校验的问题,因为有可能他的前缀或者其他需要拼接的字段在前端传递的过程 ...

  3. NodeJs 入门到放弃 — 入门基本介绍(一)

    码文不易啊,转载请带上本文链接呀,感谢感谢 https://www.cnblogs.com/echoyya/p/14450905.html 目录 码文不易啊,转载请带上本文链接呀,感谢感谢 https ...

  4. Python中的sklearn--KFold与StratifiedKFold

    KFold划分数据集的原理:根据n_split直接进行划分 StratifiedKFold划分数据集的原理:划分后的训练集和验证集中类别分布尽量和原数据集一样 #导入相关packages from s ...

  5. 你要是还学不会,请提刀来见 Typora+PicGo+Gitee + node.js 打造个人高效稳定优雅图床

    你要是还学不会,请提刀来见 Typora+PicGo+Gitee + node.js 打造个人高效稳定优雅图床 经过前面两弹的介绍,相信大家对图床都不陌生了吧, 但是小魔童觉得这样做法还是不方便,使用 ...

  6. kali 下的邮件发送工具 swaks

    kali 下的邮件发送工具 swaks Swaks 是一个功能强大,灵活,可编写脚本,面向事务的 SMTP 测试工具,目前 Swaks 托管在私有 svn 存储库中. 官方项目 http://jetm ...

  7. Linux磁盘分区格式化和扩容

    Note:根据各系统上磁盘的类型不同,磁盘命名规则也会不同:例如/dev/xvd,/dev/sd,/dev/vd,/dev/hd 目录 磁盘格式化 MBR格式 GPT分区 磁盘扩容 MBR格式扩容 G ...

  8. 后端程序员之路 53、A Tour of Go-3

    #method    - Methods        - Go does not have classes. However, you can define methods on types.    ...

  9. Java RPC 框架 Solon 1.3.7 发布,增强Cloud接口能力范围

    Solon 是一个微型的Java RPC开发框架.项目从2018年启动以来,参考过大量前人作品:历时两年,4000多次的commit:内核保持0.1m的身材,超高的跑分,良好的使用体验.支持:RPC. ...

  10. Percona XtraDB Cluster之流量控制

    什么是流量控制? Percona XtraDB Cluster具有一种称为流控制的自调节机制.该机制有助于避免集群中最弱/最慢的成员明显落后于集群中其他成员的情况. 当集群成员在写数据很慢(同时又继续 ...