深入剖析 mybatis 原理(一)

# 前言

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

1. 回憶JDBC

首先,樓主想和大家一起回憶學(xué)習(xí)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 是不是覺得很親切呢?只是現(xiàn)如今,我們再也不會這么寫代碼了,都是在Spring和Mybatis 中整合了,一個 userinfoMapper.selectOne(id) 方法就搞定了上面的這么多代碼,這都是我們今天的主角 Mybatis 的功勞,而他主要做的事情,就是封裝了上面的除SQL語句之外的重復(fù)代碼,為什么說是重復(fù)代碼呢?因為這些代碼,細想一下,都是不變的。

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

實際上,Mybatis 只做了兩件事情:

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

2. 從 Mybatis 的一個 Demo 案例開始

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

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

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

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>

  <!--環(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 到哪里去找到這些語句-->
  <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>

好了,我們的測試代碼就這么多,運行一下測試類:

結(jié)果正確,打印了2次,因為我們使用了兩種不同的方式來執(zhí)行SQL。

那么,我們就從這個簡單的例子來看看 Mybatis 是如何運行的。

3. 深入源碼之前的理論知識

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

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

我們再來看看我們的測試代碼:

Main 測試類代碼

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

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

在這里, 樓主覺得有必要講講這幾個類的生命周期:

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

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

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

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

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

4. 開始深入源碼

我們一行一行看。

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

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

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

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

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

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

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

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

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

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

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

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

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

返回了一個默認的 DefaultSqlSessionFactory 對象。

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

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

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

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

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

該方法以下幾個步驟:

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

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

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

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

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

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

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

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

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

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

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

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

我們再看看默認執(zhí)行器的構(gòu)造方法,2個參數(shù),一個是 Configuration, 一個是事務(wù)對象。該構(gòu)造器調(diào)用了父類 BaseExecutor 的構(gòu)造器,我們看看該方法實現(xiàn):

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

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

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

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

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

接下來,就要看看他的執(zhí)行過程。

6. SqlSession 執(zhí)行過程

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

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

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

該方法首先根據(jù) key或者說 id 從 configuration 中取出 SQL 聲明對象, 那么是如何取出的呢?我們知道,我們的SQL語句再XML中編輯的時候,都有一個key,加上我們?nèi)薅惷统闪艘粋€唯一的id,我們進入到該方法查看:

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

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

那么 buildAllStatements 方法做了什么呢?

注意看注釋(大意):解析緩存中所有未處理的語句節(jié)點。當(dāng)所有的映射器都被添加時,建議調(diào)用這個方法,因為它提供了快速失敗語句驗證。意思是如果鏈表中任何一個不為空,則拋出異常,是一種快速失敗的機制。那么這些是什么時候添加進鏈表的呢?答案是catch的時候,看代碼:

這個時候會將錯誤的語句添加進該鏈表中。

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

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

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

會有5個屬性被賦值,sql語句,參數(shù),

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

并將配置對象賦值給該類。

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

回到我們的 getBoundSql 方法:

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

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

該方法步驟:

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

我們重點關(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ù)庫查詢。
  5. 如果需要延遲加載,則開始加載,最后清空加載隊列。
  6. 如果配置文件中的緩存范圍是聲明范圍,則清空本地緩存。
  7. 最后返回list。

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

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

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

mybatis 的SqlSession 有4大對象:

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

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

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

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

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

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

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

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

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

第一行,獲取連接器。

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

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

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

該方法以下幾個步驟:

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

我們主要看看第一步:

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

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

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

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

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

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

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

如果返回值長度等于1,返回第一個值,否則返回本身。

至此,終于返回了一個List。不容易啊!!!!最后在返回值的時候執(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)部主要是清除緩存,校驗是否清除 Statements。然后執(zhí)行 transaction.close()方法,重置事務(wù)(重置事務(wù)的autoCommit 屬性為true),最后調(diào)用 connection.close() 方法,和我們JDBC 一樣,關(guān)閉連接,但實際上,該connection 被代理了,被 PooledConnection 連接池代理了,在該代理的invoke方法中,會將該connection從連接池集合刪除,在創(chuàng)建一個新的連接放在集合中。最后回到 SimpleExecurtor 的 close 方法中,在執(zhí)行完事務(wù)的close 方法后,在finally塊中將所有應(yīng)用置為null,等待GC回收。清除工作也就完畢了。

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

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

我們再看看這行代碼,這么一行簡單的代碼里面 mybatis 為我們封裝了無數(shù)的調(diào)用。可不簡單。

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

7. 總結(jié)

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

還記得我們剛開始說的嗎?mybatis 做的什么工作?

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

還有Mybatis 的運行過程?

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

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

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

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

到這里,我們這篇文章就結(jié)束了,當(dāng)然,大家肯定還想知道 getMapper 的原理是怎么回事,其實我們開始說過,getMapper 更加的面向?qū)ο螅彩菍ι厦娴拇a的封裝。篇幅有限,我們將在下篇文章中詳細解析。

good luck!!!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內(nèi)容