深入剖析 mybatis 原理(一)

# 前言

在java程序員的世界里,最熟悉的開(kāi)源軟件除了 Spring,Tomcat,還有誰(shuí)呢?當(dāng)然是 Mybatis 了,今天樓主是來(lái)和大家一起分析他的原理的。

1. 回憶JDBC

首先,樓主想和大家一起回憶學(xué)習(xí)JDBC的那段時(shí)光:

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 是不是覺(jué)得很親切呢?只是現(xiàn)如今,我們?cè)僖膊粫?huì)這么寫代碼了,都是在Spring和Mybatis 中整合了,一個(gè) userinfoMapper.selectOne(id) 方法就搞定了上面的這么多代碼,這都是我們今天的主角 Mybatis 的功勞,而他主要做的事情,就是封裝了上面的除SQL語(yǔ)句之外的重復(fù)代碼,為什么說(shuō)是重復(fù)代碼呢?因?yàn)檫@些代碼,細(xì)想一下,都是不變的。

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

實(shí)際上,Mybatis 只做了兩件事情:

  1. 根據(jù) JDBC 規(guī)范 建立與數(shù)據(jù)庫(kù)的連接。
  2. 通過(guò)反射打通Java對(duì)象和數(shù)據(jù)庫(kù)參數(shù)和返回值之間相互轉(zhuǎn)化的關(guān)系。

2. 從 Mybatis 的一個(gè) Demo 案例開(kāi)始

此次樓主從 github 上 clone 了mybatis 的源碼,過(guò)程比Spring源碼順利,主要注意一點(diǎn):在 IDEA 編輯器中(Eclipse 樓主不知道),需要排除 src/test/java/org/apache/ibatis/submitted 包,防止編譯錯(cuò)誤。

樓主在源碼中寫了一個(gè)Demo,給大家看一下目錄結(jié)構(gòu):

圖片中的紅框部分是樓主自己新增的,然后看看代碼:

JavaBean代碼
Mapper 接口代碼
Main 測(cè)試類代碼

再看看 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>

  <!--環(huán)境-->
  <environments default="development">
    <environment id="development"><!--采用jdbc 的事務(wù)管理模式-->
      <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 到哪里去找到這些語(yǔ)句-->
  <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>

好了,我們的測(cè)試代碼就這么多,運(yùn)行一下測(cè)試類:

結(jié)果正確,打印了2次,因?yàn)槲覀兪褂昧藘煞N不同的方式來(lái)執(zhí)行SQL。

那么,我們就從這個(gè)簡(jiǎn)單的例子來(lái)看看 Mybatis 是如何運(yùn)行的。

3. 深入源碼之前的理論知識(shí)

再深入源碼之前,樓主想先來(lái)一波理論知識(shí),避免因進(jìn)入源碼的汪洋大海導(dǎo)致迷失方向。

首先, Mybatis 的運(yùn)行可以分為2個(gè)部分,第一部分是讀取配置文件創(chuàng)建 Configuration 對(duì)象, 用以創(chuàng)建 SqlSessionFactroy, 第二部分是 SQLSession 的執(zhí)行過(guò)程.

我們?cè)賮?lái)看看我們的測(cè)試代碼:

Main 測(cè)試類代碼

這是一個(gè)和我們平時(shí)使用不同的方式, 但如果細(xì)心觀察,會(huì)發(fā)現(xiàn), 實(shí)際上在 Spring 和 Mybatis 整合的框架中也是這么使用的, 只是 Spring 的 IOC 機(jī)制幫助我們屏蔽了創(chuàng)建對(duì)象的過(guò)程而已. 如果我們忘記創(chuàng)建對(duì)象的過(guò)程, 這段代碼就是我們平時(shí)使用的代碼.

那么,我們就來(lái)看看這段代碼, 首先創(chuàng)建了一個(gè)流, 用于讀取配置文件, 然后使用流作為參數(shù), 使用 SqlSessionaFactoryBuilder 創(chuàng)建了一個(gè) SqlSessionFactory 對(duì)象,然后使用該對(duì)象獲取一個(gè) SqlSession, 調(diào)用 SqlSession 的 selectOne 方法 獲取了返回值,或者 調(diào)用了 SqlSession 的 getMapper 方法獲取了一個(gè)代理對(duì)象, 調(diào)用代理對(duì)象的 selectById 方法 獲取返回值.

在這里, 樓主覺(jué)得有必要講講這幾個(gè)類的生命周期:

  1. SqlSessionaFactoryBuilder 該類主要用于創(chuàng)建 SqlSessionFactory, 并給與一個(gè)流對(duì)象, 該類使用了創(chuàng)建者模式, 如果是手動(dòng)創(chuàng)建該類(這種方式很少了,除非像樓主這種測(cè)試代碼), 那么建議在創(chuàng)建完畢之后立即銷毀.

  2. SqlSessionFactory 該類的作用了創(chuàng)建 SqlSession, 從名字上我們也能看出, 該類使用了工廠模式, 每次應(yīng)用程序訪問(wèn)數(shù)據(jù)庫(kù), 我們就要通過(guò) SqlSessionFactory 創(chuàng)建 SqlSession, 所以SqlSessionFactory 和整個(gè) Mybatis 的生命周期是相同的. 這也告訴我們不同創(chuàng)建多個(gè)同一個(gè)數(shù)據(jù)的 SqlSessionFactory, 如果創(chuàng)建多個(gè), 會(huì)消耗盡數(shù)據(jù)庫(kù)的連接資源, 導(dǎo)致服務(wù)器夯機(jī). 應(yīng)當(dāng)使用單例模式. 避免過(guò)多的連接被消耗, 也方便管理.

  3. SqlSession 那么是什么 SqlSession 呢? SqlSession 相當(dāng)于一個(gè)會(huì)話, 就像 HTTP 請(qǐng)求中的會(huì)話一樣, 每次訪問(wèn)數(shù)據(jù)庫(kù)都需要這樣一個(gè)會(huì)話, 大家可能會(huì)想起了 JDBC 中的 Connection, 很類似,但還是有區(qū)別的, 何況現(xiàn)在幾乎所有的連接都是使用的連接池技術(shù), 用完后直接歸還而不會(huì)像 Session 一樣銷毀. 注意:他是一個(gè)線程不安全的對(duì)象, 在設(shè)計(jì)多線程的時(shí)候我們需要特別的當(dāng)心, 操作數(shù)據(jù)庫(kù)需要注意其隔離級(jí)別, 數(shù)據(jù)庫(kù)鎖等高級(jí)特性, 此外, 每次創(chuàng)建的 SqlSession 都必須及時(shí)關(guān)閉它, 它長(zhǎng)期存在就會(huì)使數(shù)據(jù)庫(kù)連接池的活動(dòng)資源減少,對(duì)系統(tǒng)性能的影響很大, 我們一般在 finally 塊中將其關(guān)閉. 還有, SqlSession 存活于一個(gè)應(yīng)用的請(qǐng)求和操作,可以執(zhí)行多條 Sql, 保證事務(wù)的一致性.

  4. Mapper 映射器, 正如我們編寫的那樣, Mapper 是一個(gè)接口, 沒(méi)有任何實(shí)現(xiàn)類, 他的作用是發(fā)送 SQL, 然后返回我們需要的結(jié)果. 或者執(zhí)行 SQL 從而更改數(shù)據(jù)庫(kù)的數(shù)據(jù), 因此它應(yīng)該在 SqlSession 的事務(wù)方法之內(nèi), 在 Spring 管理的 Bean 中, Mapper 是單例的。

大家應(yīng)該還看見(jiàn)了另一種方式, 就是上面的我們不常見(jiàn)到的方式,其實(shí), 這個(gè)方法更貼近Mybatis底層原理,只是該方法還是不夠面向?qū)ο螅?使用字符串當(dāng)key的方式也不易于IDE 檢查錯(cuò)誤。我們常用的還是getMapper方法。

4. 開(kāi)始深入源碼

我們一行一行看。

首先根據(jù)maven的classes目錄下的配置文件并創(chuàng)建流,然后創(chuàng)建 SqlSessionFactoryBuilder 對(duì)象,該類結(jié)構(gòu)如下:

可以看到該類只有一個(gè)方法并且被重載了9次,而且沒(méi)有任何屬性,可見(jiàn)該類唯一的功能就是通過(guò)配置文件創(chuàng)建 SqlSessionFactory。那我們就緊跟來(lái)看看他的build方法:

該方法,默認(rèn)環(huán)境為null, 屬性也為null,調(diào)用了自己的另一個(gè)重載build方法,我們看看該方法。

  /**
   * 構(gòu)建SqlSession 工廠
   *
   * @param inputStream xml 配置文件
   * @param environment 默認(rèn)null
   * @param properties 默認(rèn)null
   * @return 工廠
   */
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 創(chuàng)建XML解析器
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 創(chuàng)建 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個(gè)步驟,第一,根據(jù)給定的參數(shù)創(chuàng)建一個(gè) XMLConfigBuilder XML配置對(duì)象,第二,調(diào)用重載的 build 方法。并將上一行返回的 Configuration 對(duì)象作為參數(shù)。我們首先看看創(chuàng)建 XMLConfigBuilder 的過(guò)程。

首先還是調(diào)用了自己的構(gòu)造方法,參數(shù)是 XPathParser 對(duì)象, 環(huán)境(默認(rèn)是null),Properties (默認(rèn)是null),然后調(diào)用了父類的構(gòu)造方法并傳入 Configuration 對(duì)象,注意,Configuration 的構(gòu)造器做了很多的工作,或者說(shuō)他的默認(rèn)構(gòu)造器做了很多的工作。我們看看他的默認(rèn)構(gòu)造器:

該構(gòu)造器主要是注冊(cè)別名,并放入到一個(gè)HashMap中,這些別名在解析XML配置文件的時(shí)候會(huì)用到。如果平時(shí)注意mybatis配置文件的話,這些別名應(yīng)該都非常的熟悉了。

我們回到 XMLConfigBuilderd 的構(gòu)造方法中,也就是他的父類 BaseBuilder 構(gòu)造方法,該方法如下:

主要是一些賦值過(guò)程,主要將剛剛創(chuàng)建的 Configuration 對(duì)象和他的屬性賦值到 XMLConfigBuilder 對(duì)象中。

我們回到 SqlSessionFactoryBuilder 的 build 方法中,此時(shí)已經(jīng)創(chuàng)建了 XMLConfigBuilder 對(duì)象,并調(diào)用該對(duì)象的 parse 方法,我們看看該方法實(shí)現(xiàn):

首先判斷了最多只能解析一次,然后調(diào)用 XPathParser 的 evalNode 方法,該方法返回了 XNode 對(duì)象 ,而XNode 對(duì)象就和我們平時(shí)使用的 Dom4j 的 node 對(duì)象差不多,我們就不深究了,總之是解析XML 配置文件,加載 DOM 樹(shù),返回 DOM 節(jié)點(diǎn)對(duì)象。然后調(diào)用 parseConfiguration 方法,我們看看該方法:

該方法的作用是解析剛剛的DOM節(jié)點(diǎn),可以看到我們熟悉的一些標(biāo)簽,比如:properties,settings,objectWrapperFactory,mappers。我們重點(diǎn)看看最后一行 mapperElement 方法,其余的方法,大家如果又興趣自己也可以看看,mapperElement 方法如下:

該方法循環(huán)了 mapper 元素,如果有 “package” 標(biāo)簽,則獲取value值,并添加進(jìn)映射器集合Map中,該Map如何保存呢,找到包所有class,并將Class對(duì)象作為key,MapperProxyFactory 對(duì)象作為 value 保存, MapperProxyFactory 類中有2個(gè)屬性,一個(gè)是 Class<T> mapperInterface ,也就是接口的類名,一個(gè) Map<Method, MapperMethod> methodCache 方法緩存。我們回到 XMLConfigBuilder 的 mapperElement 方法中, 如果沒(méi)有 “package” 屬性,則嘗試獲取 “resource”, “url”,“class”屬性,并一個(gè)個(gè)判斷,最后都會(huì)和 “package”方法一樣,調(diào)用 configuration.addMapper 方法。將 namespace 屬性和配置文件關(guān)聯(lián)。

在執(zhí)行完 parseConfiguration 方法后,也就完成了 XMLConfigBuilder 對(duì)象的 parse 方法,調(diào)用重載方法 build :

返回了一個(gè)默認(rèn)的 DefaultSqlSessionFactory 對(duì)象。

至此,解析配置文件的工作就結(jié)束了,此時(shí)創(chuàng)建了 SqlSessionFactory 對(duì)象和 Configuration 對(duì)象,這兩個(gè)對(duì)象都是單例的,且他們的聲明周期和 Mybatis 是一致的。 Configuration 對(duì)象中包含了 Mybatis 配置文件中的所有信息,在后面大有用處,SqlSessionFactory 將創(chuàng)建后面所有的SqlSession對(duì)象,可見(jiàn)其重要性。

可以看到,創(chuàng)建 SqlSessionFactory 對(duì)象是比較簡(jiǎn)單的,然后,SqlSession 的執(zhí)行過(guò)程就不那么簡(jiǎn)單了。我們繼續(xù)往下看。

5. SqlSession 創(chuàng)建過(guò)程

我們接下來(lái)要看看 SqlSession 的創(chuàng)建過(guò)程和運(yùn)行過(guò)程,首先調(diào)用了 sqlSessionFactory.openSession() 方法。該方法默認(rèn)實(shí)現(xiàn)類是 DefaultSqlSessionFactory ,我們看看該方法如何被重寫的。

調(diào)用了自身的 openSessionFromDataSource 方法,注意,參數(shù)中 configuration 獲取了默認(rèn)的執(zhí)行器 “SIMPLE”,自動(dòng)提交我們沒(méi)有配置,默認(rèn)是false,我們進(jìn)入到 openSessionFromDataSource 方法查看:

該方法以下幾個(gè)步驟:

  1. 獲取配置文件中的環(huán)境,也就是我們配置的 <environments default="development">標(biāo)簽,并根據(jù)環(huán)境獲取事務(wù)工廠,事務(wù)工廠會(huì)創(chuàng)建一個(gè)事務(wù)對(duì)象,而 configurationye 則會(huì)根據(jù)事務(wù)對(duì)象和執(zhí)行器類型創(chuàng)建一個(gè)執(zhí)行器。最后返回一個(gè)默認(rèn)的 DefaultSqlSession 對(duì)象。 可以說(shuō),這段代碼,就是根據(jù)配置文件創(chuàng)建 SqlSession 的核心地帶。我們一步步看代碼,首先從配置文件中取出剛剛解析的環(huán)境對(duì)象。

然后根據(jù)環(huán)境對(duì)象獲取事務(wù)工廠,如果配置文件中沒(méi)有配置,則創(chuàng)建一個(gè) ManagedTransactionFactory 對(duì)象直接返回。否則調(diào)用環(huán)境對(duì)象的 getTransactionFactory 方法,該方法和我們配置的一樣返回了一個(gè) JdbcTransactionFactory,而實(shí)際上,TransactionFactory 只有2個(gè)實(shí)現(xiàn)類,一個(gè)是 ManagedTransactionFactory ,一個(gè)是 JdbcTransactionFactory。

我們回到 openSessionFromDataSource 方法,獲取了 JdbcTransactionFactory 后,調(diào)用 JdbcTransactionFactory 的 newTransaction 方法創(chuàng)建一個(gè)事務(wù)對(duì)象,參數(shù)是數(shù)據(jù)源,level 是null, 自動(dòng)提交還是false。newTransaction 創(chuàng)建了一個(gè) JdbcTransaction 對(duì)象,我們看看該類的構(gòu)造:

可以看到,該類都是有關(guān)連接和事務(wù)的方法,比如commit,openConnection,rollback,和JDBC 的connection 功能很相似。而我們剛剛看到的level是什么呢?在源碼中我們看到了答案:

就是 “事務(wù)的隔離級(jí)別”。并且該事務(wù)對(duì)象還包含了JDBC 的Connection 對(duì)象和 DataSource 數(shù)據(jù)源對(duì)象,好親切啊,可見(jiàn)這個(gè)事務(wù)對(duì)象就是JDBC的事務(wù)的封裝。

繼續(xù)回到 openSessionFromDataSource 方,法此時(shí)已經(jīng)創(chuàng)建好事務(wù)對(duì)象。接下來(lái)將事務(wù)對(duì)象執(zhí)行器作為參數(shù)執(zhí)行 configuration 的 newExecutor 方法來(lái)獲取一個(gè) 執(zhí)行器類。我們看看該方法實(shí)現(xiàn):

首先,該方法判斷給定的執(zhí)行類型是否為null,如果為null,則使用默認(rèn)的執(zhí)行器, 也就是 ExecutorType.SIMPLE,然后根據(jù)執(zhí)行的類型來(lái)創(chuàng)建不同的執(zhí)行器,默認(rèn)是 SimpleExecutor 執(zhí)行器,這里樓主需要解釋以下執(zhí)行器:

Mybatis有三種基本的Executor執(zhí)行器,SimpleExecutor、ReuseExecutor、BatchExecutor。

  1. SimpleExecutor:每執(zhí)行一次update或select,就開(kāi)啟一個(gè)Statement對(duì)象,用完立刻關(guān)閉Statement對(duì)象。

  2. ReuseExecutor:執(zhí)行update或select,以sql作為key查找Statement對(duì)象,存在就使用,不存在就創(chuàng)建,用完后,不關(guān)閉Statement對(duì)象,而是放置于Map<String, Statement>內(nèi),供下一次使用。簡(jiǎn)言之,就是重復(fù)使用Statement對(duì)象。

  3. BatchExecutor:執(zhí)行update(沒(méi)有select,JDBC批處理不支持select),將所有sql都添加到批處理中(addBatch()),等待統(tǒng)一執(zhí)行(executeBatch()),它緩存了多個(gè)Statement對(duì)象,每個(gè)Statement對(duì)象都是addBatch()完畢后,等待逐一執(zhí)行executeBatch()批處理。與JDBC批處理相同。

作用范圍:Executor的這些特點(diǎn),都嚴(yán)格限制在SqlSession生命周期范圍內(nèi)。

我們?cè)倏纯茨J(rèn)執(zhí)行器的構(gòu)造方法,2個(gè)參數(shù),一個(gè)是 Configuration, 一個(gè)是事務(wù)對(duì)象。該構(gòu)造器調(diào)用了父類 BaseExecutor 的構(gòu)造器,我們看看該方法實(shí)現(xiàn):

該類包裝了事務(wù)對(duì)象,延遲加載的隊(duì)列,本地緩存,永久緩存,配置對(duì)象,還包裝了自己。

回到 newExecutor 方法,判斷是否使用緩存,默認(rèn)是true, 則將剛剛的執(zhí)行器包裝到新的 CachingExecutor 緩存執(zhí)行器中。最后將執(zhí)行器添加到所有的攔截器中(如果配置了話),我們這里沒(méi)有配置。

現(xiàn)在,我們回到 openSessionFromDataSource 方法,我們已經(jīng)有了執(zhí)行器,此時(shí)創(chuàng)建 DefaultSqlSession 對(duì)象,攜帶 configuration, executor, autoCommit 三個(gè)參數(shù),該構(gòu)造器就是簡(jiǎn)單的賦值過(guò)程。我們有必要看看該類的結(jié)構(gòu):

該類包含了常用的所有方法,包括事務(wù)方法,可以說(shuō),該類封裝了執(zhí)行器和事務(wù)類。而執(zhí)行器才是具體的執(zhí)行工作人員。

至此,我們已經(jīng)完成了 SqlSession 的創(chuàng)建過(guò)程。

接下來(lái),就要看看他的執(zhí)行過(guò)程。

6. SqlSession 執(zhí)行過(guò)程

我們創(chuàng)建了一個(gè)map,并放入了參數(shù),重點(diǎn)看紅框部分,我們鉆進(jìn)去看看。selectOne 方法:

該方法實(shí)際上還是調(diào)用了selectList方法,最后取得了List中的第一個(gè),如果返回值長(zhǎng)度大于1,則拋出異常。啊,原來(lái),經(jīng)常出現(xiàn)的異常就是這么來(lái)的啊,終于知道你是怎么回事了。我們也看的出來(lái),重點(diǎn)再 selectList 方法中,我們進(jìn)入看看:

該方法攜帶了3個(gè)參數(shù),SQL 聲明的key,參數(shù)Map,默認(rèn)分頁(yè)對(duì)象(不分頁(yè)),注意,mybatis 分頁(yè)是假分頁(yè),即一次返回所有到內(nèi)存中,再進(jìn)行提取,如果數(shù)據(jù)過(guò)多,可能引起OOM。我們繼續(xù)向下走:

該方法首先根據(jù) key或者說(shuō) id 從 configuration 中取出 SQL 聲明對(duì)象, 那么是如何取出的呢?我們知道,我們的SQL語(yǔ)句再XML中編輯的時(shí)候,都有一個(gè)key,加上我們?nèi)薅惷?,就成了一個(gè)唯一的id,我們進(jìn)入到該方法查看:

該方法調(diào)用了自身的 getMappedStatement 方法,默認(rèn)需要驗(yàn)證SQL語(yǔ)句是否正確,也就是 buildAllStatements 方法,最后從繼承了 HashMap 的StrictMap 中取出 value,這個(gè)StrictMap 有個(gè)注意的地方,他基本擴(kuò)展了HashMap 的方法,我們重點(diǎn)看看他的get方法:

如何擴(kuò)展呢?如果返回值是null,則拋出異常,JDK中HashMap 可是不拋出異常的,如果 value是 Ambiguity 類型,也拋出異常,說(shuō)明 key 值不夠清晰。

那么 buildAllStatements 方法做了什么呢?

注意看注釋(大意):解析緩存中所有未處理的語(yǔ)句節(jié)點(diǎn)。當(dāng)所有的映射器都被添加時(shí),建議調(diào)用這個(gè)方法,因?yàn)樗峁┝丝焖偈≌Z(yǔ)句驗(yàn)證。意思是如果鏈表中任何一個(gè)不為空,則拋出異常,是一種快速失敗的機(jī)制。那么這些是什么時(shí)候添加進(jìn)鏈表的呢?答案是catch的時(shí)候,看代碼:

這個(gè)時(shí)候會(huì)將錯(cuò)誤的語(yǔ)句添加進(jìn)該鏈表中。

我們回到 selectList 方法,此時(shí)已經(jīng)返回了 MappedStatement 對(duì)象,這個(gè)時(shí)候該執(zhí)行器出場(chǎng)了,調(diào)用執(zhí)行器的query方法,攜帶映射聲明,包裝過(guò)的參數(shù)對(duì)象,分頁(yè)對(duì)象。那么如何包裝參數(shù)對(duì)象呢?我們看看 wrapCollection 方法:

該方法首先判斷是否是集合類型,如果是,則創(chuàng)建一個(gè)自定義Map,key是collection,value是集合,如果不是,并且還是數(shù)組,則key為array,都不滿足則直接返回該對(duì)象。那么我們?cè)撨M(jìn)入 query 一探究竟:

進(jìn)入 CachingExecutor 的query 方法,首先根據(jù)參數(shù)獲取 BoundSql 對(duì)象,最終會(huì)調(diào)用 StaticSqlSource 的 getBoundSql 方法,該方法會(huì)構(gòu)造一個(gè) BoundSql 對(duì)象,構(gòu)造過(guò)程是什么樣子的呢?

會(huì)有5個(gè)屬性被賦值,sql語(yǔ)句,參數(shù),

參數(shù)是我們剛剛傳遞的,那么SQL 是怎么來(lái)的呢,答案是在 XMLConfigBuilder 的 parseConfiguration 方法中,通過(guò)層層調(diào)用,最終執(zhí)行 StaticSqlSource 的構(gòu)造方法,將mapper 文件中的Sql解析到該類中,最后會(huì)將XML 中的 #{id} 構(gòu)造成一個(gè)ParameterMapping 對(duì)象,格式入下:

并將配置對(duì)象賦值給該類。

回到 BoundSql 的構(gòu)造器,首先賦值SQL, 參數(shù)映射對(duì)象數(shù)組,參數(shù)對(duì)象,默認(rèn)的額外參數(shù),還有一個(gè)元數(shù)據(jù)參數(shù)。

回到我們的 getBoundSql 方法:

我們已經(jīng)有了參數(shù)綁定對(duì)象,該對(duì)象中有SQL語(yǔ)句,參數(shù)。繼續(xù)向下執(zhí)行,從該對(duì)象獲取參數(shù)映射集合,如果為空,則再次創(chuàng)建一個(gè) BoundSql 對(duì)象。接著循環(huán)參數(shù),先獲取 resultMap id,如果有,則從配置對(duì)下中獲取resultMap 對(duì)象,如果不為null,則修改 hasNestedResultMaps 為 true。最后返回 BoundSql 對(duì)象。

我們回到 CachingExecutor 的 query 方法, 我們已經(jīng)有了sql綁定對(duì)象, 接下來(lái)創(chuàng)建一個(gè)緩存key,根據(jù)sql綁定對(duì)象,方法聲明對(duì)象,參數(shù)對(duì)象,分頁(yè)對(duì)象,注意:mybatis 一級(jí)緩存默認(rèn)為true,二級(jí)緩存默認(rèn)false。創(chuàng)建緩存的過(guò)程很簡(jiǎn)單,就是將所有的參數(shù)的key或者id構(gòu)造該 CacheKey 對(duì)象,使該對(duì)象唯一。最后執(zhí)行query方法:

該方法步驟:

  1. 獲取緩存,如果沒(méi)u偶,則執(zhí)行代理執(zhí)行器的query方法,如果有,且需要清空了,則清空緩存(也就是Map)。
  2. 如果該方法聲明使用緩存并且結(jié)果處理器為null,則校驗(yàn)參數(shù),如果方法聲明使存儲(chǔ)過(guò)程,且所有參數(shù)有任意一個(gè)不是輸入類型,則拋出異常。意思是當(dāng)為存儲(chǔ)過(guò)程時(shí),確保不能有輸出參數(shù)。
  3. 調(diào)用 TransactionalCacheManager 事務(wù)緩存處理器執(zhí)行 getObject 方法,如果返回值時(shí)null,則調(diào)用代理執(zhí)行器的query方法,最后添加進(jìn)事務(wù)緩存處理器。

我們重點(diǎn)關(guān)注代理執(zhí)行器的query方法,也就是我們 SimpleExecutor 執(zhí)行器。該方法如下:

  1. 首先判斷執(zhí)行器狀態(tài)是否關(guān)閉。
  2. 判斷是否需要清除緩存。
  3. 判斷結(jié)果處理器是否為null,如果不是null,則返回null,如果不是,則從本地緩存中取出。
  4. 如果返回的list不是null,則處理緩存和參數(shù)。否則調(diào)用queryFromDatabase 方法從數(shù)據(jù)庫(kù)查詢。
  5. 如果需要延遲加載,則開(kāi)始加載,最后清空加載隊(duì)列。
  6. 如果配置文件中的緩存范圍是聲明范圍,則清空本地緩存。
  7. 最后返回list。

可以看出,我們重點(diǎn)要關(guān)注的是 queryFromDatabase 方法,其余的方法都是和緩存相關(guān),但如果沒(méi)有從數(shù)據(jù)庫(kù)取出來(lái),緩存也沒(méi)什么用。進(jìn)入該方法查看:

我們關(guān)注紅框部分。

該方法創(chuàng)建了一個(gè)聲明處理器,然后調(diào)用了 prepareStatement 方法,最后調(diào)用了聲明處理器的query方法,注意,這個(gè)聲明處理器有必要說(shuō)一下:

mybatis 的SqlSession 有4大對(duì)象:

  1. Executor代表執(zhí)行器,由它調(diào)度StatementHandler、ParameterHandler、ResultSetHandler等來(lái)執(zhí)行對(duì)應(yīng)的SQL。其中StatementHandler是最重要的。
  2. StatementHandler的作用是使用數(shù)據(jù)庫(kù)的Statement(PreparedStatement)執(zhí)行操作,它是四大對(duì)象的核心,起到承上啟下的作用,許多重要的插件都是通過(guò)攔截它來(lái)實(shí)現(xiàn)的。
  3. ParamentHandler是用來(lái)處理SQL參數(shù)的。
  4. ResultSetHandler是進(jìn)行數(shù)據(jù)集的封裝返回處理的,它相當(dāng)復(fù)雜,好在我們不常用它。

好,我們繼續(xù)查看 configuration 是如何創(chuàng)建 StatementHandler 對(duì)象的。我們看看他的 newStatementHandler 方法:

首先根據(jù)方法聲明類型創(chuàng)建一個(gè)聲明處理器,有最簡(jiǎn)單的,有預(yù)編譯的,有存儲(chǔ)過(guò)程的,在我們這個(gè)方法中,創(chuàng)建了一個(gè)預(yù)編譯的方法聲明對(duì)象,這個(gè)對(duì)象的構(gòu)造器對(duì) configuration 等很多參數(shù)進(jìn)行的賦值。我們還是看看吧:

我們看到了剛剛提到了parameterHandler和resultSetHandler。

回到 newStatementHandler 方法,需要執(zhí)行下面的攔截器鏈的pluginAll方法,由于我們這里沒(méi)有配置攔截器,該方法也就結(jié)束了。攔截器就是實(shí)現(xiàn)了Interceptor接口的類,國(guó)內(nèi)著名的分頁(yè)插件pagehelper就是這個(gè)原理,在mybais 源碼里,有一個(gè)插件使用的例子,我們可以隨便看看:

執(zhí)行了Plugin 的靜態(tài) wrap 方法,包裝目標(biāo)類(也就是方法聲明處理器),該靜態(tài)方法如下:

這里就是動(dòng)態(tài)代理的知識(shí)了,獲取目標(biāo)類的接口,最后執(zhí)行攔截器的invoke方法。有機(jī)會(huì)和大家再一起探討如何編寫攔截器插件。這里由于篇幅原因就不展開(kāi)了。

我們回到 newStatementHandler 方法,此時(shí),如果我們有攔截器,返回的應(yīng)該是被層層包裝的代理類,但今天我們沒(méi)有。返回了一個(gè)普通的方法聲明器。

執(zhí)行 prepareStatement 方法,攜帶方法聲明器,日志對(duì)象。

第一行,獲取連接器。

從事務(wù)管理器中獲取連接器(該方法中還需要設(shè)置是否自動(dòng)提交,隔離級(jí)別)。如果我們的事務(wù)日志是debug級(jí)別,則創(chuàng)建一個(gè)日志代理對(duì)象,代理Connection。

回到 prepareStatement 方法,看第二行,開(kāi)始讓預(yù)編譯處理器預(yù)編譯sql(也就是讓connection預(yù)編譯),我看看看是如何執(zhí)行的。注意,我們沒(méi)有配置timeout。因此返回null。

進(jìn)入 RoutingStatementHandler 的 prepare 方法,調(diào)用了代理類的 PreparedStatementHandler 的prepare方法,該方法實(shí)現(xiàn)入下:

該方法以下幾個(gè)步驟:

  1. 實(shí)例化SQL,也就是調(diào)用connection 啟動(dòng) prepareStatement 方法。我們熟悉的JDBC方法。
  2. 設(shè)置超時(shí)時(shí)間。
  3. 設(shè)置fetchSize ,作用是,執(zhí)行查詢時(shí),一次從服務(wù)器端拿多少行的數(shù)據(jù)到本地jdbc客戶端這里來(lái)。
  4. 最后返回映射聲明處理器。

我們主要看看第一步:

有沒(méi)有很親切,我們看到我們?cè)趧傞_(kāi)始回憶JDBC編程的 connection.prepareStatement 代碼,由此證明mybatis 就是封裝了 JDBC。首先判斷是否含有返回主鍵的功能,如果有,則看 keyColumnNames 是否存在,如果不存在,取第一個(gè)列為主鍵。最后執(zhí)行else 語(yǔ)句,開(kāi)始預(yù)編譯。注意:此connection 已經(jīng)被動(dòng)態(tài)代理封裝過(guò)了,因此會(huì)調(diào)用 invoke 方法打印日志。最后返回聲明處理器對(duì)象。

我們回到 SimpleExecutor 的 prepareStatement 方法, 執(zhí)行第三行 handler.parameterize(stmt),該方法其實(shí)也是委托了 PreparedStatementHandler 來(lái)執(zhí)行,而 PreparedStatementHandler 則委托了 DefaultParameterHandler 執(zhí)行 setParameters 方法,我們看看該方法:

首先獲取參數(shù)映射集合,然后從配置對(duì)象創(chuàng)建一個(gè)元數(shù)據(jù)對(duì)象,最后從元數(shù)據(jù)對(duì)象取出參數(shù)值。再?gòu)膮?shù)映射對(duì)象中取出類型處理器,最后將類型處理器和參數(shù)處理器關(guān)聯(lián)。我們看看最后一行代碼:

還是JDBC。而這個(gè)下標(biāo)的順序則就是參數(shù)映射的數(shù)組下標(biāo)。

終于,在準(zhǔn)備了那么多之后,我們回到 doQuery 方法,有了預(yù)編譯好的聲明處理器,接下來(lái)就是執(zhí)行了。當(dāng)然還是調(diào)用了PreparedStatementHandler 的query方法。

可以看到,直接執(zhí)行JDBC 的 execute 方法,注意,該對(duì)象也被日志對(duì)象代理了,做打印日志工作,和清除工作。如果方法名稱是 “executeQuery” 則返回 ResultSet 并代理該對(duì)象。 否則直接執(zhí)行。我們繼續(xù)看看DefaultResultSetHandler 的 handleResultSets 是如何執(zhí)行的:

首先調(diào)用 getFirstResultSet 方法獲取包裝過(guò)的 ResultSet ,然后從映射器中獲取 resultMap 和resultSet,如果不為null,則調(diào)用 handleResultSet 方法,將返回值和resultMaps處理添加進(jìn)multipleResults list中 ,然后做一些清除工作。最后調(diào)用 collapseSingleResultList 方法,該方法內(nèi)容如下:

如果返回值長(zhǎng)度等于1,返回第一個(gè)值,否則返回本身。

至此,終于返回了一個(gè)List。不容易?。。。?!最后在返回值的時(shí)候執(zhí)行關(guān)閉 Statement 等操作。我們還需要關(guān)注一下 SqlSession 的 close 方法,該方法是事務(wù)最后是否生效的關(guān)鍵,當(dāng)然真正的執(zhí)行者是executor,在
CachingExecutor 的close 方法中:

該方法決定了到底是commit 還是rollback,最后執(zhí)行代理執(zhí)行器的 close 方法,也就是 SimpleExecutor 的close方法,該方法內(nèi)容入下:

首先執(zhí)行rollback方法,該方法內(nèi)部主要是清除緩存,校驗(yàn)是否清除 Statements。然后執(zhí)行 transaction.close()方法,重置事務(wù)(重置事務(wù)的autoCommit 屬性為true),最后調(diào)用 connection.close() 方法,和我們JDBC 一樣,關(guān)閉連接,但實(shí)際上,該connection 被代理了,被 PooledConnection 連接池代理了,在該代理的invoke方法中,會(huì)將該connection從連接池集合刪除,在創(chuàng)建一個(gè)新的連接放在集合中。最后回到 SimpleExecurtor 的 close 方法中,在執(zhí)行完事務(wù)的close 方法后,在finally塊中將所有應(yīng)用置為null,等待GC回收。清除工作也就完畢了。

到這里 SqlSession的運(yùn)行就基本結(jié)束了。

最后返回到我們的main方法,打印輸出。

我們?cè)倏纯催@行代碼,這么一行簡(jiǎn)單的代碼里面 mybatis 為我們封裝了無(wú)數(shù)的調(diào)用??刹缓?jiǎn)單。

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

7. 總結(jié)

今天我們從一個(gè)小demo開(kāi)始 debug mybatis 源碼,從如何加載配置文件,到如何創(chuàng)建SqlSedssionFactory,再到如何創(chuàng)建 SqlSession,再到 SqlSession 是如何執(zhí)行的,我們知道了他們的生命周期。其中創(chuàng)建SqlSessionFactory 和 SqlSession 是比較簡(jiǎn)單的,執(zhí)行SQL并封裝返回值是比較復(fù)雜的,因?yàn)檫€需要配置事務(wù),日志,插件等工作。

還記得我們剛開(kāi)始說(shuō)的嗎?mybatis 做的什么工作?

  1. 根據(jù) JDBC 規(guī)范 建立與數(shù)據(jù)庫(kù)的連接。
  2. 通過(guò)反射打通Java對(duì)象和數(shù)據(jù)庫(kù)參數(shù)和返回值之間相互轉(zhuǎn)化的關(guān)系。

還有Mybatis 的運(yùn)行過(guò)程?

  1. 讀取配置文件創(chuàng)建 Configuration 對(duì)象, 用以創(chuàng)建 SqlSessionFactroy.
  2. SQLSession 的執(zhí)行過(guò)程.

我們也知道了其實(shí)在mybatis 層層封裝下,真正做事情的是 StatementHandler,他下面的各個(gè)實(shí)現(xiàn)類分別代表著不同的SQL聲明,我們看看他有哪些屬性就知道了:

該類可以說(shuō)囊括了所有執(zhí)行SQL的必備屬性:配置,對(duì)象工廠,類型處理器,結(jié)果集處理器,參數(shù)處理器,SQL執(zhí)行器,映射器(保存這個(gè)SQL 所有相關(guān)屬性的地方,比放入SQL語(yǔ)句,參數(shù),返回值類型,配置,id,聲明類型等等), 分頁(yè)對(duì)象, 綁定SQL與參數(shù)對(duì)象。有了這些東西,還有什么SQL執(zhí)行不了的呢?

當(dāng)然,StatementHandler 只是 SqlSession 4 大對(duì)象的其中之一,還有Executor 執(zhí)行器,他負(fù)責(zé)調(diào)度 StatementHandler,ParameterHandler,ResultHandler 等來(lái)執(zhí)行對(duì)應(yīng)的SQL,而 StatementHandler 的作用是使用數(shù)據(jù)庫(kù)的 Statement(PreparedStatement ) 執(zhí)行操作,他是4大對(duì)象的核心,起到承上啟下的作用。ParameterHandler 就是封裝了對(duì)參數(shù)的處理,ResultHandler 封裝了對(duì)結(jié)果級(jí)別的處理。

到這里,我們這篇文章就結(jié)束了,當(dāng)然,大家肯定還想知道 getMapper 的原理是怎么回事,其實(shí)我們開(kāi)始說(shuō)過(guò),getMapper 更加的面向?qū)ο?,但也是?duì)上面的代碼的封裝。篇幅有限,我們將在下篇文章中詳細(xì)解析。

good luck?。。。?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容