阅读 1125

MybatisPlus源码详解

开始

时间有限,能力有限,如有不正确的地方,欢迎指正。

说到 Mybatis-Plus,想要了解它的源码,就要知道Mybatis-Plus在项目中做了什么。这个框架还是很好用的, 很简单,而且也比较火,所以这里就从MyBatis-Plus简介里复制一下它的特性。

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

总结一下,Mybatis-Plus在项目中做了三件事。

  1. 代码生成器。
  2. 启动时操作(数据库配置,Mapper扫描等)。
  3. 项目中CRUD操作。

这里主要分析一下启动时,和在项目中CRUD时,Mybatis-Plus是如何工作的。

启动时

如果想要在项目启动时,配置自动生效,就需要知道如何写一个SpringBoot Starter,刚好之前有自己写过一个Starter。 在 mybatis-plus-boot-starterresources文件夹下有一个META-INF文件夹

  • additional-spring-configuration-metadata.json 用于 我们在properties或者yml文件中,写Mybatis—plus相关配置时提示。
  • spring.factories 在项目启动时,进行自动配置。里面配置的自动启动类# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration

MybatisPlusAutoConfiguration

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})//系统中有指定的类
@ConditionalOnSingleCandidate(DataSource.class)//容器中只有一个指定的Bean,或者这个Bean是首选Bean
@EnableConfigurationProperties(MybatisPlusProperties.class)//为带有@ConfigurationProperties注解的Bean提供有效的支持
@AutoConfigureAfter(DataSourceAutoConfiguration.class)//将一个配置类在另一个配置类之后加载
public class MybatisPlusAutoConfiguration implements InitializingBean {
复制代码

这个类由于实现了InitializingBean接口,得到了afterPropertiesSet方法,在Bean初始化后,会自动调用。 还有三个标注了 @ConditionalOnMissingBean 注解的方法。

这个方法在没有配置SqlSessionFactory时会由SpringBoot创建Bean,并且保存到容器中。


这个方法在没有配置SqlSessionTemplate时会由SpringBoot创建Bean,并且保存到容器中。

这个方法在没有配置MapperFactoryBean时会由SpringBoot创建Bean,并且保存到容器中。

一进方法 com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory 就有一句 MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();

MybatisSqlSessionFactoryBean

这个类实现了三个接口FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>

  1. FactoryBean:说明用到了工厂模式
  2. InitializingBeanafterPropertiesSet 在属性设置完成时调用(在Bean创建完成时)调用
  3. ApplicationListener是一个监听器,监听的是ApplicationContext初始化或者刷新事件,当初始化或者刷新时调用。 Parses all the unprocessed statement nodes in the cache. It is recommended to call this method once all the mappers are added as it provides fail-fast statement validation. 用来刷新MappedStatement

然后看com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean#afterPropertiesSet这个方法, 在这个方法里,调用了com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean#buildSqlSessionFactory

buildSqlSessionFactory

简单的说就是 创建一个SqlSessionFactory实例,其实在这个方法里干了很多事情。

  1. 首先解析Mybatis的配置。
  2. 无配置启动 相关配置
  3. 初始化 id-work 以及 打印 Banner
  4. 设置元数据相关 如果用户没有配置 dbType 则自动获取
  5. 自定义枚举类扫描处理
  6. 扫描别名包
  7. 添加拦截器插件
  8. 如果是xml配置,解析xml配置文件
  9. 根据mapperLocations解析Mapper文件
  10. 最后是创建SqlSessionFactory ,并返回。

这里有一个问题,在配置类里配置了,数据库类型为Mysql,但是还是为Other。

  @Bean
    public GlobalConfig globalConfig() {
        GlobalConfig conf = new GlobalConfig();
        DbConfig dbConfig = new DbConfig();
        dbConfig.setDbType(DbType.MYSQL);
        conf.setDbConfig(dbConfig);
        return conf;
    }
复制代码

因为这个对象是new出来的,没有被Spring管理。如果改为Resource或者Autowired注入,启动报错。 但是没有关系,因为它会根据Connection的MetaData自动判断数据库类型。

接下来看这个方法的这里。 重要的是这一句xmlMapperBuilder.parse();

这个parse方法用来解析xml文件。 在框起来的地方,解析接口文件。

org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement 因为是null,所以没有进入,这个方法主要用于解析使用InsertSelectUpdateDeleteSelectProviderInsertProviderUpdateProviderDeleteProvider这八个注解的方法,因为项目中没有,所以是null 真正注入是这里
com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject this.getMethodList(mapperClass)调用的是下面的注入器
com.baomidou.mybatisplus.core.injector.DefaultSqlInjector#getMethodList

com.baomidou.mybatisplus.core.injector.AbstractMethod#inject
com.baomidou.mybatisplus.core.injector.AbstractMethod#injectMappedStatement 接下来看一个deleteById,Mybatis-Plus自动注入基本 CURD的秘密就在这里 当然,最后还有一次校验 org.springframework.dao.support.DaoSupport org.mybatis.spring.mapper.MapperFactoryBean#checkDaoConfig

项目中CRUD操作

在项目中使用Mybatis-Plus,除去基本的CRUD操作,基本都是使用Lambda表达式拼装SQL。Mybatis-Plus的AR模式,暂时不讨论。
下面就从一个简单的查询SQL来看看,Mybatis-Plus是如何拼装SQL的。 这个是Mybatis-Plus项目的单元测试

  • com.baomidou.mybatisplus.test.MybatisTest#test

调用的是这个方法

  • com.baomidou.mybatisplus.core.mapper.BaseMapper#selectCount

说到Mybatis的CRUD操作,就不得不说两个类。

  • com.baomidou.mybatisplus.core.override.MybatisMapperProxy
  • com.baomidou.mybatisplus.core.override.MybatisMapperProxyFactory

MybatisMapperProxy实现了InvocationHandler接口,采用了JDK动态代理。 而MybatisMapperProxyFactory这个类把所有生成的Mapper都绑定了代理。 在org.mybatis.spring.SqlSessionTemplate#getMapper调用方法获取的,都是JDK动态代理生产的类。

断点调试

先获取Mapper 获取代理类

获取SQL

  • com.baomidou.mybatisplus.core.conditions.AbstractWrapper#doIt
  • com.baomidou.mybatisplus.core.conditions.segments.MergeSegments#add
  • com.baomidou.mybatisplus.core.override.MybatisMapperMethod#MybatisMapperMethod
  • org.apache.ibatis.binding.MapperMethod.SqlCommand#SqlCommand
  • org.apache.ibatis.binding.MapperMethod.SqlCommand#resolveMappedStatement


可以看出来,查询语句是先拼接好了前面一部分,然后根据lambda表达式,动态拼接查询条件。所以Mybatis-Plus如果要实现使用lambda多表联接查询,难度不小。

由于这个方法是BaseMapper有的,在启动时就添加到了Configuration中,所以直接可以找到。

在这个方法中 org.apache.ibatis.binding.MapperMethod.MethodSignature#MethodSignature 设置方法签名。

执行SQLcom.baomidou.mybatisplus.core.override.MybatisMapperMethod#execute 可能sqlSegment为"",columnMap为null。因为我后来断点到这里,都是空的。(具体原因,暂时不清楚。)

SQL执行流程

这里可以看到,Mybatis的一级缓存是怎么回事,每次查询都会 createCacheKey,第二次查询如果命中,就走缓存,如果没有命中,才会走基本查询。而且每次查询先调用的是CachingExecutor,没有命中缓存之后才是BaseExecutor

  • org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
  • org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)
  • org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
  • org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
  • org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
  • org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
  • org.apache.ibatis.executor.BaseExecutor#queryFromDatabase


总结

为什么想到看源码呢?其实有两个原因,一是好奇为什么不用写基础CRUD,一个BaseMapper就可以通用。二是lambda表达式写SQL很爽,想看看能不能改造一下支持多表联接。 虽然说是Mybatis-Plus源码阅读,其实有大部分是Mybatis的源码,阅读源码确实有很多好处,至少现在知道了SQL是如何注入的,Mybatis的一级缓存是什么,JDK动态代理等等。对Mybatis-Plus也有了更深入的了解。

构思:如果要支持多表联接查询,就要新增加一个基础方法,SQL语句只有一个SELECT,然后加入leftJoin,rightJoin,join,on,using,union等方法,从表到参数,全部动态生成。或许可行。

虽然分析了很多,可是这只是大概的主流程,很多细节没有分析到,肯定有遗漏的地方。比如com.baomidou.mybatisplus.core.enums.SqlMethod。更多的知识点还是大家一起去发现吧。 再次声明,时间有限,能力有限,如有错误地方,欢迎指正。
转载请标明。

本文使用 mdnice 排版

关注下面的标签,发现更多相似文章
评论