1. 程式人生 > >Mybatis ----- 剖析Mybatis原理(一) 【摘抄】

Mybatis ----- 剖析Mybatis原理(一) 【摘抄】

摘自下面公眾號,記錄方便學習

# 前言

在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,給大家看一下目錄結構:

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

再看看 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 的執行過程。

我們再來看看我們的測試程式碼:

這是一個和我們平時使用不同的方式, 但如果細心觀察,會發現, 實際上在 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, 保證事務的一致性。

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 的執行過程就不那麼簡單了。

好了由於篇幅過長,下一屆繼續講解SqlSession 建立過程和SqlSession 執行過程。