深入剖析 mybatis 原理(一)

本贴最后更新于 2567 天前,其中的信息可能已经时移俗易

# 前言

在 java 程序员的世界里,最熟悉的开源软件除了 Spring,Tomcat,还有谁呢?当然是 Mybatis 了,今天楼主是来和大家一起分析他的原理的。

1. 回忆 JDBC

首先,楼主想和大家一起回忆学习 JDBC 的那段时光:

package cn.think.in.java.jdbc;

public class JdbcDemo {

  private Connection getConnection() {
    Connection connection = null;
    try {
      Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
      String url = "jdbc:sqlserver://192.168.0.251:1433;DatabaseName=test";
      String user = "sa";
      String password = "$434343%";
      connection = DriverManager.getConnection(url, user, password);

    } catch (Exception e) {
      e.printStackTrace();
    }
    return connection;
  }

  public UserInfo getRole(Long id) throws SQLException {
    Connection connection = getConnection();
    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
      ps = connection.prepareStatement("select * from user_info where id = ?");
      ps.setLong(1, id);
      rs = ps.executeQuery();
      while (rs.next()) {
        Long roleId = rs.getLong("id");
        String userName = rs.getString("username");
        String realname = rs.getString("realname");
        UserInfo userInfo = new UserInfo();
        userInfo.id = roleId.intValue();
        userInfo.username = userName;
        userInfo.realname = realname;
        return userInfo;
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      connection.close();
      ps.close();
      rs.close();
    }
    return null;
  }

  public static void main(String[] args) throws SQLException {
    JdbcDemo jdbcDemo = new JdbcDemo();
    UserInfo userInfo = jdbcDemo.getRole(1L);
    System.out.println(userInfo);
  }
}


看着这么多 try catch finally 是不是觉得很亲切呢?只是现如今,我们再也不会这么写代码了,都是在 Spring 和 Mybatis 中整合了,一个 userinfoMapper.selectOne(id) 方法就搞定了上面的这么多代码,这都是我们今天的主角 Mybatis 的功劳,而他主要做的事情,就是封装了上面的除 SQL 语句之外的重复代码,为什么说是重复代码呢?因为这些代码,细想一下,都是不变的。

那么,Mybatis 做了哪些事情呢?

实际上,Mybatis 只做了两件事情:

  1. 根据 JDBC 规范 建立与数据库的连接。
  2. 通过反射打通 Java 对象和数据库参数和返回值之间相互转化的关系。

2. 从 Mybatis 的一个 Demo 案例开始

此次楼主从 github 上 clone 了 mybatis 的源码,过程比 Spring 源码顺利,主要注意一点:在 IDEA 编辑器中(Eclipse 楼主不知道),需要排除 src/test/java/org/apache/ibatis/submitted 包,防止编译错误。

楼主在源码中写了一个 Demo,给大家看一下目录结构:

图片中的红框部分是楼主自己新增的,然后看看代码:

JavaBean 代码

Mapper 接口代码

Main 测试类代码

再看看 mybatis-config.xml 配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

  <properties><!--定义属性值-->
    <property name="driver" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
    <property name="url" value="jdbc:sqlserver://192.168.0.122:1433;DatabaseName=test"/>
    <property name="username" value="sa"/>
    <property name="password" value="434343"/>
  </properties>

  <settings>
    <setting name="cacheEnabled" value="true"/>
  </settings>

  <!-- 类型别名 -->
  <typeAliases>
    <typeAlias alias="userInfo" type="org.apache.ibatis.mybatis.UserInfo"/>
  </typeAliases>

  <!--环境-->
  <environments default="development">
    <environment id="development"><!--采用jdbc 的事务管理模式-->
      <transactionManager type="JDBC">
        <property name="..." value="..."/>
      </transactionManager>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>

  <!--映射器  告诉 MyBatis 到哪里去找到这些语句-->
  <mappers>
    <mapper resource="UserInfoMapper.xml"/>
  </mappers>

</configuration

UserInfoMapper.xml 配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="org.apache.ibatis.mybatis.UserInfoMapper">

  <select id="selectById" parameterType="int" resultType="org.apache.ibatis.mybatis.UserInfo">
    SELECT * FROM user_info  WHERE  id = #{id}
  </select>
</mapper>

好了,我们的测试代码就这么多,运行一下测试类:

结果正确,打印了 2 次,因为我们使用了两种不同的方式来执行 SQL。

那么,我们就从这个简单的例子来看看 Mybatis 是如何运行的。

3. 深入源码之前的理论知识

再深入源码之前,楼主想先来一波理论知识,避免因进入源码的汪洋大海导致迷失方向。

首先, Mybatis 的运行可以分为 2 个部分,第一部分是读取配置文件创建 Configuration 对象, 用以创建 SqlSessionFactroy, 第二部分是 SQLSession 的执行过程.

我们再来看看我们的测试代码:

Main 测试类代码

这是一个和我们平时使用不同的方式, 但如果细心观察,会发现, 实际上在 Spring 和 Mybatis 整合的框架中也是这么使用的, 只是 Spring 的 IOC 机制帮助我们屏蔽了创建对象的过程而已. 如果我们忘记创建对象的过程, 这段代码就是我们平时使用的代码.

那么,我们就来看看这段代码, 首先创建了一个流, 用于读取配置文件, 然后使用流作为参数, 使用 SqlSessionaFactoryBuilder 创建了一个 SqlSessionFactory 对象,然后使用该对象获取一个 SqlSession, 调用 SqlSession 的 selectOne 方法 获取了返回值,或者 调用了 SqlSession 的 getMapper 方法获取了一个代理对象, 调用代理对象的 selectById 方法 获取返回值.

在这里, 楼主觉得有必要讲讲这几个类的生命周期:

  1. SqlSessionaFactoryBuilder 该类主要用于创建 SqlSessionFactory, 并给与一个流对象, 该类使用了创建者模式, 如果是手动创建该类(这种方式很少了,除非像楼主这种测试代码), 那么建议在创建完毕之后立即销毁.

  2. SqlSessionFactory 该类的作用了创建 SqlSession, 从名字上我们也能看出, 该类使用了工厂模式, 每次应用程序访问数据库, 我们就要通过 SqlSessionFactory 创建 SqlSession, 所以 SqlSessionFactory 和整个 Mybatis 的生命周期是相同的. 这也告诉我们不同创建多个同一个数据的 SqlSessionFactory, 如果创建多个, 会消耗尽数据库的连接资源, 导致服务器夯机. 应当使用单例模式. 避免过多的连接被消耗, 也方便管理.

  3. SqlSession 那么是什么 SqlSession 呢? SqlSession 相当于一个会话, 就像 HTTP 请求中的会话一样, 每次访问数据库都需要这样一个会话, 大家可能会想起了 JDBC 中的 Connection, 很类似,但还是有区别的, 何况现在几乎所有的连接都是使用的连接池技术, 用完后直接归还而不会像 Session 一样销毁. 注意:他是一个线程不安全的对象, 在设计多线程的时候我们需要特别的当心, 操作数据库需要注意其隔离级别, 数据库锁等高级特性, 此外, 每次创建的 SqlSession 都必须及时关闭它, 它长期存在就会使数据库连接池的活动资源减少,对系统性能的影响很大, 我们一般在 finally 块中将其关闭. 还有, SqlSession 存活于一个应用的请求和操作,可以执行多条 Sql, 保证事务的一致性.

  4. Mapper 映射器, 正如我们编写的那样, Mapper 是一个接口, 没有任何实现类, 他的作用是发送 SQL, 然后返回我们需要的结果. 或者执行 SQL 从而更改数据库的数据, 因此它应该在 SqlSession 的事务方法之内, 在 Spring 管理的 Bean 中, Mapper 是单例的。

大家应该还看见了另一种方式, 就是上面的我们不常见到的方式,其实, 这个方法更贴近 Mybatis 底层原理,只是该方法还是不够面向对象, 使用字符串当 key 的方式也不易于 IDE 检查错误。我们常用的还是 getMapper 方法。

4. 开始深入源码

我们一行一行看。

首先根据 maven 的 classes 目录下的配置文件并创建流,然后创建 SqlSessionFactoryBuilder 对象,该类结构如下:

可以看到该类只有一个方法并且被重载了 9 次,而且没有任何属性,可见该类唯一的功能就是通过配置文件创建 SqlSessionFactory。那我们就紧跟来看看他的 build 方法:

该方法,默认环境为 null, 属性也为 null,调用了自己的另一个重载 build 方法,我们看看该方法。

  /**
   * 构建SqlSession 工厂
   *
   * @param inputStream xml 配置文件
   * @param environment 默认null
   * @param properties 默认null
   * @return 工厂
   */
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 创建XML解析器
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 创建 session 工厂
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

可以看到该方法只有 2 个步骤,第一,根据给定的参数创建一个 XMLConfigBuilder XML 配置对象,第二,调用重载的 build 方法。并将上一行返回的 Configuration 对象作为参数。我们首先看看创建 XMLConfigBuilder 的过程。

首先还是调用了自己的构造方法,参数是 XPathParser 对象, 环境(默认是 null),Properties (默认是 null),然后调用了父类的构造方法并传入 Configuration 对象,注意,Configuration 的构造器做了很多的工作,或者说他的默认构造器做了很多的工作。我们看看他的默认构造器:

该构造器主要是注册别名,并放入到一个 HashMap 中,这些别名在解析 XML 配置文件的时候会用到。如果平时注意 mybatis 配置文件的话,这些别名应该都非常的熟悉了。

我们回到 XMLConfigBuilderd 的构造方法中,也就是他的父类 BaseBuilder 构造方法,该方法如下:

主要是一些赋值过程,主要将刚刚创建的 Configuration 对象和他的属性赋值到 XMLConfigBuilder 对象中。

我们回到 SqlSessionFactoryBuilder 的 build 方法中,此时已经创建了 XMLConfigBuilder 对象,并调用该对象的 parse 方法,我们看看该方法实现:

首先判断了最多只能解析一次,然后调用 XPathParser 的 evalNode 方法,该方法返回了 XNode 对象 ,而 XNode 对象就和我们平时使用的 Dom4j 的 node 对象差不多,我们就不深究了,总之是解析 XML 配置文件,加载 DOM 树,返回 DOM 节点对象。然后调用 parseConfiguration 方法,我们看看该方法:

该方法的作用是解析刚刚的 DOM 节点,可以看到我们熟悉的一些标签,比如:properties,settings,objectWrapperFactory,mappers。我们重点看看最后一行 mapperElement 方法,其余的方法,大家如果又兴趣自己也可以看看,mapperElement 方法如下:

该方法循环了 mapper 元素,如果有 “package” 标签,则获取 value 值,并添加进映射器集合 Map 中,该 Map 如何保存呢,找到包所有 class,并将 Class 对象作为 key,MapperProxyFactory 对象作为 value 保存, MapperProxyFactory 类中有 2 个属性,一个是 Class mapperInterface ,也就是接口的类名,一个 Map<Method, MapperMethod> methodCache 方法缓存。我们回到 XMLConfigBuilder 的 mapperElement 方法中, 如果没有 “package” 属性,则尝试获取 “resource”, “url”,“class”属性,并一个个判断,最后都会和 “package”方法一样,调用 configuration.addMapper 方法。将 namespace 属性和配置文件关联。

在执行完 parseConfiguration 方法后,也就完成了 XMLConfigBuilder 对象的 parse 方法,调用重载方法 build :

返回了一个默认的 DefaultSqlSessionFactory 对象。

至此,解析配置文件的工作就结束了,此时创建了 SqlSessionFactory 对象和 Configuration 对象,这两个对象都是单例的,且他们的声明周期和 Mybatis 是一致的。 Configuration 对象中包含了 Mybatis 配置文件中的所有信息,在后面大有用处,SqlSessionFactory 将创建后面所有的 SqlSession 对象,可见其重要性。

可以看到,创建 SqlSessionFactory 对象是比较简单的,然后,SqlSession 的执行过程就不那么简单了。我们继续往下看。

5. SqlSession 创建过程

我们接下来要看看 SqlSession 的创建过程和运行过程,首先调用了 sqlSessionFactory.openSession() 方法。该方法默认实现类是 DefaultSqlSessionFactory ,我们看看该方法如何被重写的。

调用了自身的 openSessionFromDataSource 方法,注意,参数中 configuration 获取了默认的执行器 “SIMPLE”,自动提交我们没有配置,默认是 false,我们进入到 openSessionFromDataSource 方法查看:

该方法以下几个步骤:

  1. 获取配置文件中的环境,也就是我们配置的 标签,并根据环境获取事务工厂,事务工厂会创建一个事务对象,而 configurationye 则会根据事务对象和执行器类型创建一个执行器。最后返回一个默认的 DefaultSqlSession 对象。 可以说,这段代码,就是根据配置文件创建 SqlSession 的核心地带。我们一步步看代码,首先从配置文件中取出刚刚解析的环境对象。

然后根据环境对象获取事务工厂,如果配置文件中没有配置,则创建一个 ManagedTransactionFactory 对象直接返回。否则调用环境对象的 getTransactionFactory 方法,该方法和我们配置的一样返回了一个 JdbcTransactionFactory,而实际上,TransactionFactory 只有 2 个实现类,一个是 ManagedTransactionFactory ,一个是 JdbcTransactionFactory。

我们回到 openSessionFromDataSource 方法,获取了 JdbcTransactionFactory 后,调用 JdbcTransactionFactory 的 newTransaction 方法创建一个事务对象,参数是数据源,level 是 null, 自动提交还是 false。newTransaction 创建了一个 JdbcTransaction 对象,我们看看该类的构造:

可以看到,该类都是有关连接和事务的方法,比如 commit,openConnection,rollback,和 JDBC 的 connection 功能很相似。而我们刚刚看到的 level 是什么呢?在源码中我们看到了答案:

就是 “事务的隔离级别”。并且该事务对象还包含了 JDBC 的 Connection 对象和 DataSource 数据源对象,好亲切啊,可见这个事务对象就是 JDBC 的事务的封装。

继续回到 openSessionFromDataSource 方,法此时已经创建好事务对象。接下来将事务对象执行器作为参数执行 configuration 的 newExecutor 方法来获取一个 执行器类。我们看看该方法实现:

首先,该方法判断给定的执行类型是否为 null,如果为 null,则使用默认的执行器, 也就是 ExecutorType.SIMPLE,然后根据执行的类型来创建不同的执行器,默认是 SimpleExecutor 执行器,这里楼主需要解释以下执行器:

Mybatis 有三种基本的 Executor 执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。

  1. SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。

  2. ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement> 内,供下一次使用。简言之,就是重复使用 Statement 对象。

  3. BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。与 JDBC 批处理相同。

作用范围:Executor 的这些特点,都严格限制在 SqlSession 生命周期范围内。

我们再看看默认执行器的构造方法,2 个参数,一个是 Configuration, 一个是事务对象。该构造器调用了父类 BaseExecutor 的构造器,我们看看该方法实现:

该类包装了事务对象,延迟加载的队列,本地缓存,永久缓存,配置对象,还包装了自己。

回到 newExecutor 方法,判断是否使用缓存,默认是 true, 则将刚刚的执行器包装到新的 CachingExecutor 缓存执行器中。最后将执行器添加到所有的拦截器中(如果配置了话),我们这里没有配置。

现在,我们回到 openSessionFromDataSource 方法,我们已经有了执行器,此时创建 DefaultSqlSession 对象,携带 configuration, executor, autoCommit 三个参数,该构造器就是简单的赋值过程。我们有必要看看该类的结构:

该类包含了常用的所有方法,包括事务方法,可以说,该类封装了执行器和事务类。而执行器才是具体的执行工作人员。

至此,我们已经完成了 SqlSession 的创建过程。

接下来,就要看看他的执行过程。

6. SqlSession 执行过程

我们创建了一个 map,并放入了参数,重点看红框部分,我们钻进去看看。selectOne 方法:

该方法实际上还是调用了 selectList 方法,最后取得了 List 中的第一个,如果返回值长度大于 1,则抛出异常。啊,原来,经常出现的异常就是这么来的啊,终于知道你是怎么回事了。我们也看的出来,重点再 selectList 方法中,我们进入看看:

该方法携带了 3 个参数,SQL 声明的 key,参数 Map,默认分页对象(不分页),注意,mybatis 分页是假分页,即一次返回所有到内存中,再进行提取,如果数据过多,可能引起 OOM。我们继续向下走:

该方法首先根据 key 或者说 id 从 configuration 中取出 SQL 声明对象, 那么是如何取出的呢?我们知道,我们的 SQL 语句再 XML 中编辑的时候,都有一个 key,加上我们全限定类名,就成了一个唯一的 id,我们进入到该方法查看:

该方法调用了自身的 getMappedStatement 方法,默认需要验证 SQL 语句是否正确,也就是 buildAllStatements 方法,最后从继承了 HashMap 的 StrictMap 中取出 value,这个 StrictMap 有个注意的地方,他基本扩展了 HashMap 的方法,我们重点看看他的 get 方法:

如何扩展呢?如果返回值是 null,则抛出异常,JDK 中 HashMap 可是不抛出异常的,如果 value 是 Ambiguity 类型,也抛出异常,说明 key 值不够清晰。

那么 buildAllStatements 方法做了什么呢?

注意看注释(大意):解析缓存中所有未处理的语句节点。当所有的映射器都被添加时,建议调用这个方法,因为它提供了快速失败语句验证。意思是如果链表中任何一个不为空,则抛出异常,是一种快速失败的机制。那么这些是什么时候添加进链表的呢?答案是 catch 的时候,看代码:

这个时候会将错误的语句添加进该链表中。

我们回到 selectList 方法,此时已经返回了 MappedStatement 对象,这个时候该执行器出场了,调用执行器的 query 方法,携带映射声明,包装过的参数对象,分页对象。那么如何包装参数对象呢?我们看看 wrapCollection 方法:

该方法首先判断是否是集合类型,如果是,则创建一个自定义 Map,key 是 collection,value 是集合,如果不是,并且还是数组,则 key 为 array,都不满足则直接返回该对象。那么我们该进入 query 一探究竟:

进入 CachingExecutor 的 query 方法,首先根据参数获取 BoundSql 对象,最终会调用 StaticSqlSource 的 getBoundSql 方法,该方法会构造一个 BoundSql 对象,构造过程是什么样子的呢?

会有 5 个属性被赋值,sql 语句,参数,

参数是我们刚刚传递的,那么 SQL 是怎么来的呢,答案是在 XMLConfigBuilder 的 parseConfiguration 方法中,通过层层调用,最终执行 StaticSqlSource 的构造方法,将 mapper 文件中的 Sql 解析到该类中,最后会将 XML 中的 #{id} 构造成一个 ParameterMapping 对象,格式入下:

并将配置对象赋值给该类。

回到 BoundSql 的构造器,首先赋值 SQL, 参数映射对象数组,参数对象,默认的额外参数,还有一个元数据参数。

回到我们的 getBoundSql 方法:

我们已经有了参数绑定对象,该对象中有 SQL 语句,参数。继续向下执行,从该对象获取参数映射集合,如果为空,则再次创建一个 BoundSql 对象。接着循环参数,先获取 resultMap id,如果有,则从配置对下中获取 resultMap 对象,如果不为 null,则修改 hasNestedResultMaps 为 true。最后返回 BoundSql 对象。

我们回到 CachingExecutor 的 query 方法, 我们已经有了 sql 绑定对象, 接下来创建一个缓存 key,根据 sql 绑定对象,方法声明对象,参数对象,分页对象,注意:mybatis 一级缓存默认为 true,二级缓存默认 false。创建缓存的过程很简单,就是将所有的参数的 key 或者 id 构造该 CacheKey 对象,使该对象唯一。最后执行 query 方法:

该方法步骤:

  1. 获取缓存,如果没 u 偶,则执行代理执行器的 query 方法,如果有,且需要清空了,则清空缓存(也就是 Map)。
  2. 如果该方法声明使用缓存并且结果处理器为 null,则校验参数,如果方法声明使存储过程,且所有参数有任意一个不是输入类型,则抛出异常。意思是当为存储过程时,确保不能有输出参数。
  3. 调用 TransactionalCacheManager 事务缓存处理器执行 getObject 方法,如果返回值时 null,则调用代理执行器的 query 方法,最后添加进事务缓存处理器。

我们重点关注代理执行器的 query 方法,也就是我们 SimpleExecutor 执行器。该方法如下:

  1. 首先判断执行器状态是否关闭。
  2. 判断是否需要清除缓存。
  3. 判断结果处理器是否为 null,如果不是 null,则返回 null,如果不是,则从本地缓存中取出。
  4. 如果返回的 list 不是 null,则处理缓存和参数。否则调用 queryFromDatabase 方法从数据库查询。
  5. 如果需要延迟加载,则开始加载,最后清空加载队列。
  6. 如果配置文件中的缓存范围是声明范围,则清空本地缓存。
  7. 最后返回 list。

可以看出,我们重点要关注的是 queryFromDatabase 方法,其余的方法都是和缓存相关,但如果没有从数据库取出来,缓存也没什么用。进入该方法查看:

我们关注红框部分。

该方法创建了一个声明处理器,然后调用了 prepareStatement 方法,最后调用了声明处理器的 query 方法,注意,这个声明处理器有必要说一下:

mybatis 的 SqlSession 有 4 大对象:

  1. Executor 代表执行器,由它调度 StatementHandler、ParameterHandler、ResultSetHandler 等来执行对应的 SQL。其中 StatementHandler 是最重要的。
  2. StatementHandler 的作用是使用数据库的 Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
  3. ParamentHandler 是用来处理 SQL 参数的。
  4. ResultSetHandler 是进行数据集的封装返回处理的,它相当复杂,好在我们不常用它。

好,我们继续查看 configuration 是如何创建 StatementHandler 对象的。我们看看他的 newStatementHandler 方法:

首先根据方法声明类型创建一个声明处理器,有最简单的,有预编译的,有存储过程的,在我们这个方法中,创建了一个预编译的方法声明对象,这个对象的构造器对 configuration 等很多参数进行的赋值。我们还是看看吧:

我们看到了刚刚提到了 parameterHandler 和 resultSetHandler。

回到 newStatementHandler 方法,需要执行下面的拦截器链的 pluginAll 方法,由于我们这里没有配置拦截器,该方法也就结束了。拦截器就是实现了 Interceptor 接口的类,国内著名的分页插件 pagehelper 就是这个原理,在 mybais 源码里,有一个插件使用的例子,我们可以随便看看:

执行了 Plugin 的静态 wrap 方法,包装目标类(也就是方法声明处理器),该静态方法如下:

这里就是动态代理的知识了,获取目标类的接口,最后执行拦截器的 invoke 方法。有机会和大家再一起探讨如何编写拦截器插件。这里由于篇幅原因就不展开了。

我们回到 newStatementHandler 方法,此时,如果我们有拦截器,返回的应该是被层层包装的代理类,但今天我们没有。返回了一个普通的方法声明器。

执行 prepareStatement 方法,携带方法声明器,日志对象。

第一行,获取连接器。

从事务管理器中获取连接器(该方法中还需要设置是否自动提交,隔离级别)。如果我们的事务日志是 debug 级别,则创建一个日志代理对象,代理 Connection。

回到 prepareStatement 方法,看第二行,开始让预编译处理器预编译 sql(也就是让 connection 预编译),我看看看是如何执行的。注意,我们没有配置 timeout。因此返回 null。

进入 RoutingStatementHandler 的 prepare 方法,调用了代理类的 PreparedStatementHandler 的 prepare 方法,该方法实现入下:

该方法以下几个步骤:

  1. 实例化 SQL,也就是调用 connection 启动 prepareStatement 方法。我们熟悉的 JDBC 方法。
  2. 设置超时时间。
  3. 设置 fetchSize ,作用是,执行查询时,一次从服务器端拿多少行的数据到本地 jdbc 客户端这里来。
  4. 最后返回映射声明处理器。

我们主要看看第一步:

有没有很亲切,我们看到我们在刚开始回忆 JDBC 编程的 connection.prepareStatement 代码,由此证明 mybatis 就是封装了 JDBC。首先判断是否含有返回主键的功能,如果有,则看 keyColumnNames 是否存在,如果不存在,取第一个列为主键。最后执行 else 语句,开始预编译。注意:此 connection 已经被动态代理封装过了,因此会调用 invoke 方法打印日志。最后返回声明处理器对象。

我们回到 SimpleExecutor 的 prepareStatement 方法, 执行第三行 handler.parameterize(stmt),该方法其实也是委托了 PreparedStatementHandler 来执行,而 PreparedStatementHandler 则委托了 DefaultParameterHandler 执行 setParameters 方法,我们看看该方法:

首先获取参数映射集合,然后从配置对象创建一个元数据对象,最后从元数据对象取出参数值。再从参数映射对象中取出类型处理器,最后将类型处理器和参数处理器关联。我们看看最后一行代码:

还是 JDBC。而这个下标的顺序则就是参数映射的数组下标。

终于,在准备了那么多之后,我们回到 doQuery 方法,有了预编译好的声明处理器,接下来就是执行了。当然还是调用了 PreparedStatementHandler 的 query 方法。

可以看到,直接执行 JDBC 的 execute 方法,注意,该对象也被日志对象代理了,做打印日志工作,和清除工作。如果方法名称是 “executeQuery” 则返回 ResultSet 并代理该对象。 否则直接执行。我们继续看看 DefaultResultSetHandler 的 handleResultSets 是如何执行的:

首先调用 getFirstResultSet 方法获取包装过的 ResultSet ,然后从映射器中获取 resultMap 和 resultSet,如果不为 null,则调用 handleResultSet 方法,将返回值和 resultMaps 处理添加进 multipleResults list 中 ,然后做一些清除工作。最后调用 collapseSingleResultList 方法,该方法内容如下:

如果返回值长度等于 1,返回第一个值,否则返回本身。

至此,终于返回了一个 List。不容易啊!!!!最后在返回值的时候执行关闭 Statement 等操作。我们还需要关注一下 SqlSession 的 close 方法,该方法是事务最后是否生效的关键,当然真正的执行者是 executor,在
CachingExecutor 的 close 方法中:

该方法决定了到底是 commit 还是 rollback,最后执行代理执行器的 close 方法,也就是 SimpleExecutor 的 close 方法,该方法内容入下:

首先执行 rollback 方法,该方法内部主要是清除缓存,校验是否清除 Statements。然后执行 transaction.close()方法,重置事务(重置事务的 autoCommit 属性为 true),最后调用 connection.close() 方法,和我们 JDBC 一样,关闭连接,但实际上,该 connection 被代理了,被 PooledConnection 连接池代理了,在该代理的 invoke 方法中,会将该 connection 从连接池集合删除,在创建一个新的连接放在集合中。最后回到 SimpleExecurtor 的 close 方法中,在执行完事务的 close 方法后,在 finally 块中将所有应用置为 null,等待 GC 回收。清除工作也就完毕了。

到这里 SqlSession 的运行就基本结束了。

最后返回到我们的 main 方法,打印输出。

我们再看看这行代码,这么一行简单的代码里面 mybatis 为我们封装了无数的调用。可不简单。

UserInfo userInfo1 = sqlSession.selectOne("org.apache.ibatis.mybatis.UserInfoMapper.selectById", parameter);

7. 总结

今天我们从一个小 demo 开始 debug mybatis 源码,从如何加载配置文件,到如何创建 SqlSedssionFactory,再到如何创建 SqlSession,再到 SqlSession 是如何执行的,我们知道了他们的生命周期。其中创建 SqlSessionFactory 和 SqlSession 是比较简单的,执行 SQL 并封装返回值是比较复杂的,因为还需要配置事务,日志,插件等工作。

还记得我们刚开始说的吗?mybatis 做的什么工作?

  1. 根据 JDBC 规范 建立与数据库的连接。
  2. 通过反射打通 Java 对象和数据库参数和返回值之间相互转化的关系。

还有 Mybatis 的运行过程?

  1. 读取配置文件创建 Configuration 对象, 用以创建 SqlSessionFactroy.
  2. SQLSession 的执行过程.

我们也知道了其实在 mybatis 层层封装下,真正做事情的是 StatementHandler,他下面的各个实现类分别代表着不同的 SQL 声明,我们看看他有哪些属性就知道了:

该类可以说囊括了所有执行 SQL 的必备属性:配置,对象工厂,类型处理器,结果集处理器,参数处理器,SQL 执行器,映射器(保存这个 SQL 所有相关属性的地方,比放入 SQL 语句,参数,返回值类型,配置,id,声明类型等等), 分页对象, 绑定 SQL 与参数对象。有了这些东西,还有什么 SQL 执行不了的呢?

当然,StatementHandler 只是 SqlSession 4 大对象的其中之一,还有 Executor 执行器,他负责调度 StatementHandler,ParameterHandler,ResultHandler 等来执行对应的 SQL,而 StatementHandler 的作用是使用数据库的 Statement(PreparedStatement ) 执行操作,他是 4 大对象的核心,起到承上启下的作用。ParameterHandler 就是封装了对参数的处理,ResultHandler 封装了对结果级别的处理。

到这里,我们这篇文章就结束了,当然,大家肯定还想知道 getMapper 的原理是怎么回事,其实我们开始说过,getMapper 更加的面向对象,但也是对上面的代码的封装。篇幅有限,我们将在下篇文章中详细解析。

good luck!!!!

  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 386 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • someone

    博主 我又来啦!
    博主你真的好厉害!
    虽然我看不懂
    但是!就是觉得你超级无敌牛逼!

  • someone

    你这样我会骄傲的。

  • szuhcc

    写得很好,整体思路非常清晰,理解很顺畅,感谢。❤️️

  • lushwe

    博主厉害,为啥我看不到图片