【Mybatis源码阅读】从源码角度分析mybatis核心工作流程

7,570 阅读10分钟

mybatis是当今Java项目使用最为广泛的ORM框架,免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。本文将会带大家从源码角度分析mybatis核心工作流程,知其然,更知其所以然。

源码地址:

mybatis中文注释源码

mybatis使用示例

寻找入口

要想理解完整的工作流程,肯定要从mybatis原始API入手,下面是使用原始API使用mybatis的示例代码(MyBatisTest.java):

@Test
public void testSelect() throws IOException {
    String resource = "config/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
    try {
        // 通过sqlSession先获取到Mapper接口对象
        BlogMapper mapper = session.getMapper(BlogMapper.class);
        Blog blog = mapper.selectBlogById(1688);
        System.out.println(blog);
    } finally {
        session.close();
    }
}

可以明显看出来,入口代码就是SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

配置解析

mybatis有两种配置文件,一种是全局配置文件mybatis-config.xml,另一种就是各个具体的Mapper文件xxxMapper.xml 。在上面的代码中,核心就是基于mybatis-config.xml配置文件构建出了sqlSessionFactory对象。跟进SqlSessionFactoryBuilderbuild()方法: image.png

上面代码的关键是调用了parserparse()方法,它会返回一个Configuration类。 image.png

明显可以看出来,解析配置是从configuration根节点开始解析的。具体解析过程如下: image.png

解析配置完整方法上所示,主要的解析过程包含属性、类型别名、插件、对象工厂、对象包装工厂、设置、环境、类型处理器、映射器等等。下面挑选最关键的部分具体讲解。

属性 propertiesElement()

image.png

第一个是解析<properties>标签,读取我们引入的外部配置文件。解析的最终结果就是我们会把所有的配置信息放到名为defaultsProperties对象里面,最后把XPathParserConfigurationProperties属性都设置成填充后的Properties对象。

类型别名 typeAliasesElement()

image.png

类型别名是解析<typeAliases>标签,它有两种定义方式,一种是直接定义一个类的别名,另一种是指定一个包,此时这个package下面所有的类的名字就会成为这个类全路径的别名。类的别名和类的关系,放在一个TypeAliasRegistry对象里面。

插件 pluginElement()

image.png

插件是解析<plugins>标签。<plugins>标签里面只有<plugin>标签,<plugin>标签里面只有<property>标 签。标签解析完以后,会生成一个Interceptor对象,并且添加到ConfigurationInterceptorChain属性里面,它是一个List

对象工厂 objectFactoryElement()objectWrapperFactoryElement()

image.png

这两个标签是用来实例化对象用的,分别解析的是<objectFactory><objectWrapperFactory>这两个标签,对应生成ObjectFactoryObjectWrapperFactory对象,同样设置到Configuration的属性里面。

设置 settingsElement()

image.png

设置是解析<settings>标签,首先将所有子标签全部转换成了Properties对象,然后再将相应的属性设置到Configuration中。

二级标签里面有很多的配置,比如二级缓存,延迟加载,自动生成主键这些。需要注意的是,所有的默认值都是在这里赋值的。如果我们不知道这个属性的值是什么,也可以到这一步来确认一下。 所有的值,都会赋值到Configuration的属性中。

环境 environmentsElement()

image.png

一个environment就是对应一个数据源,所以在这里我们会根据配置的<transactionManager>创建一个事务工厂,根据<dataSource>标签创建一个数据源,最后把这两个对象设置成Environment对象的属性,放到 Configuration里面。

类型处理器 typeHandlerElement()

image.png

TypeAlias一样,TypeHandler有两种配置方式,一种是单独配置一个类,一种是指定一个package。最后我们得到的是JavaTypeJdbcType,以及用来做相互映射的TypeHandler,并将其保存在 TypeHandlerRegistry对象里面。

映射器 mapperElement()

image.png image.png

映射器支持4种配置方式,包括类路径、绝对url路径、Java类名和自动扫描包方式。下面以自动扫描包为例,具体说明。查看configuration.addMappers(mapperPackage)实现: image.png image.png

查找指定包下所有的mapperClass,并注册。继续跟进addMapper(mapperClass)实现: image.png

添加映射就是将Mapper类型和对应的MapperProxyFactory关联,放到一个Map容器中。并且在这里还会去解析Mapper接口方法上的注解。具体来说是创建了一个MapperAnnotationBuilder,我们点进去看一下 parse()方法。 image.png

parseCache()parseCacheRef()方法其实是对@CacheNamespace@CacheNamespaceRef这两个注解的处理。parseStatement()方法里面的各种getAnnotation(),都是对注解的解析,比如@Options@SelectKey@ResultMap等等。

最后同样会解析成MappedStatement对象,也就是说在XML中配置,和使用注解配置,最后起到一样的效果

小结

经过上述步骤,我们主要完成了config配置文件、Mapper文件、Mapper接口上的注解的解析。我们得到了一个最重要的对象Configuration,这里面存放了全部的配置信息,它在属性里面还有各种各样的容器。最后,返回了一个DefaultSqlSessionFactory,里面持有了Configuration的实例。

创建会话

在构建好SqlSessionFactory对象,接下来创建会话了,也就是执行SqlSession session = sqlSessionFactory.openSession(),获取会话对象。具体源码如下: image.png

首先从Configuration里面拿到Enviroment,再通过Enviroment获取事务工厂TransactionFactory。接下里,通过事务工厂来产生一个事务,再生成一个执行器(事务包含在执行器里),然后生成DefaultSqlSession

创建 Transaction

如果配置的是JDBC,则会使用Connection对象的commit()rollback()close()管理事务。

如果是Spring + MyBatis,则没有必要配置,因为会直接使用applicationContext.xml里面配置数据源和事务管理器,覆盖MyBatis的配置。

创建 Executor

image.png

Executor的基本类型有三种:SIMPLEBATCHREUSE,默认是SIMPLE (settingsElement()读取默认值),他们都继承了抽象类BaseExecutor

Executor三种类型的区别是什么?

  1. SimpleExecutor:每执行一次updateselect,就开启一个Statement对象,用完立刻关闭 Statement对象。
  2. ReuseExecutor:执行updateselect,以 sql 作为key查找Statement对象,存在就使用,不存在就创建。用完后,不关闭Statement对象,而是放置于Map内, 供下一次使用。简言之,就是重复使用 Statement对象
  3. BatchExecutor:执行update(没有select,JDBC 批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个 Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与 JDBC 批处理相同。

如果配置了cacheEnabled=ture,会用装饰器模式对executor进行包装:new CachingExecutor(executor)

最后调用Executor的插件,执行对应的插件逻辑。(插件原理后续再讲)

小结

创建会话的过程,主要是获得了一个DefaultSqlSession,里面包含了一个Executor,它是SQL的执行者

获取Mapper对象

在创建好SqlSession对象之后,接下来就是获取Mapper对象了。Mapper.xml文件与Mapper类型通过namespace进行关联,Statement ID与方法名进行了关联。因此,调用Mapper的方法就能执行相应的SQL了。

namespace等于类的全路径名,Statement ID等于方法名。

image.png image.png image.png

最后调用MapperRegistrygetMapper()方法。在前面配置解析阶段,我们讲过添加映射就是将Mapper类型和对应的MapperProxyFactory关联,放到一个Map容器中。而这里就是根据Mapper类型,得到对应的MapperProxyFactory,接下来通过代理工厂就可以创建一个Mapper代理对象了。 image.png

执行SQL

由于所有的Mapper都是MapperProxy代理对象,所以任意的方法都是执行MapperProxyinvoke()方法。 image.png

invoke()方法的执行步骤主要有2步,第一步是查找MapperMethod,第二步是执行方法

查找MapperMethod

image.png

优先从缓存中获取MapperMethod,缓存中没有则创建一个。

执行方法

执行方法就是调用MapperMethodexecute()方法。 image.png

可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法。调用 convertArgsToSqlCommandParam()将参数转换为SQL的参数。

接下来,我们以查询单行记录为例,最终会执行DefaultSqlSession.selectOne()方法。 image.png

可以看到,selectOne()最终也是调用了selectList()image.png

SelectList()中,我们先根据Statement IDConfiguration中拿到 MappedStatement,这个ms上面有我们在 xml中配置的所有属性,包括idstatementTypesqlSourceuseCache、入参、出参等等。

查询

在获取到MappedStatement之后,接下就是调用执行器的query()方法了。查询是最复杂的sql处理,接下来详细分析查询的执行流程。

前面我们说到了Executor有三种基本类型,SIMPLE/REUSE/BATCH。另外还有一种包装类型CachingExecutorimage.png

如果启用了二级缓存,就会先调用CachingExecutorquery()方法,里面有缓存相关的操作,然后才是再调用基本类型的执行器,比如默认的SimpleExecutor。最终会调用BaseExecutor.query()方法: image.png

主要包含获取绑定sql、创建CacheKey和执行查询。

获取绑定sql

image.png

根据输入参数,获取绑定sql

创建CacheKey

image.png

MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]生成一个哈希码

执行查询

image.png

优先从缓存中查询,如果没有,则从数据库查询:queryFromDatabase()

数据库查询

image.png

先向缓存用占位符占位。执行查询后,移除占位符,放入数据。执行查询调用的是doQuery()方法。

执行查询 doQuery()

image.png

主要包含创建StatementHandler、准备语句和查询三步。

创建StatementHandler

image.png image.png

configuration.newStatementHandler()中,创建了一个 StatementHandler,先得到 RoutingStatementHandlerRoutingStatementHandler里面没有任何的实现,它是用来创建基本的 StatementHandler的。这里会根据MappedStatement里面的statementType决定StatementHandler的类型 。 默认是PREPARED。 接下来以预处理语句处理器(PREPARED)为例进行分析。 image.png

这里直接调用了父类BaseStatementHandler的构造方法。 image.png

在构造方法里面,重点是生成了处理参数的ParameterHandler和处理结果集的ResultSetHandler。它们都可以被插件拦截。所以在创建之后都要用拦截器包装。 image.png

准备语句

image.png 调用prepareStatement()方法对语句进行预处理。主要是根据连接准备语句和参数化处理。

查询

RoutingStatementHandlerquery()方法最终委派给PreparedStatementHandler执行。 image.png

在这里,调用了PreparedStatementexecute()方法,它的底层就是是调用了JDBC的PreparedStatement进行执行,这里就不展开讲了。我们的重点是结果集的处理。 image.png

首先我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况,一般只有一个结果集。也就是下面的这个while循环只会执行一次,然后会调用handleResultSet()方法。 image.png

如果没有配置结果处理器,则会使用默认的结果处理器DefaultResultHandler,否则使用配置的结果处理器进行处理。

小结

总之,调用Mapper对象的方法就是执行sql。首先,它会根据方法和参数得到绑定的sql,然后创建语句处理器、准备好语句等等,之后就会通过JDBC执行sql,最后处理结果集,将结果转化为Mapper方法的返回类型返回。

总结

总的来说,mybatis核心工作流程是非常清晰的。

  1. 第一步是配置解析,这一步的重点就是根据配置文件生成Configuration,基于它然后构建DefaultSqlSessionFactory对象。
  2. 第二步是创建会话,主要是获得了一个DefaultSqlSession对象,里面包含了一个Executor,它是SQL的执行者。
  3. 第三步是获取Mapper对象,这一步主要是根据第一步注册号的Mapper,创建好MapperProxyFactory代理对象。后续所有的方法调用都会调用MapperProxyFactoryinvoke()方法。
  4. 第四步是执行方法,在这里会创建语句处理器、准备好语句等等,之后就会通过JDBC执行sql,最后处理结果集,将结果转化为Mapper方法的返回类型返回。

原创不易,觉得文章写得不错的小伙伴,点个赞👍 鼓励一下吧~

欢迎关注我的开源项目:一款适用于SpringBoot的轻量级HTTP调用框架