mybatis的源碼解析(一)

寫在前面的話

隨着越來越多的Java工程師湧入開發的行列,以往的只是簡單會用已經越來越容易被這個行業所淘汰,尤其是隻能靠着開發賺取這一微薄的薪水。在公司往復開發平凡的業務SQL接口等,這些都將非常容易被淘汰,而擺脫此困境只能更加需要深入的學習,形成自己的體系和認知,以便跳出這個風口,不做平凡職業者,向頂端看齊,讓我們一起學習一起努力進步,只有這樣,薪水纔會隨着年限的增加而增加。

學習方法

學習這件事情很奇妙,有時候靜下心來好好學居然也會上癮。但偶爾間斷了幾天不學,似乎又不想要繼續學了,除了最開始的興奮,只剩下乏力和枯燥。然後感覺經常記憶只是放了一個錨:知道問題可以在哪兒解決,而不是自己去整理解決方法,遇到問題都是搜索完以後又忘卻,下次遇到又繼續去搜索。這樣往往復復什麼都沒學到,後來我總結了一些方法,我也正嘗試着使用這些方法(個人方法,不喜可跳過),希望自己以後遇到問題時也會回來回顧和修改之前的一些想法:

  1. 做筆記並學會總結 ,當學習到一個新知識時,我當場可能會截圖,或者留下一個筆記,但並沒有做好總結,以至於我面試時感覺自己好像什麼都會,又感覺什麼都不會,沒有有亮點的技術也沒留下像樣的文稿和資料。當你學會並開始做筆記時就好像把學到的知識在腦海裏過了一遍,可以深刻並在這裏留下了一個錨,以便於以後更方便尋找或者向下挖掘。
  2. 講給別人聽,當你學完了一個新知識,你可以試着講給別人聽,如果別人也能在你複述的情況下能聽懂,那麼你對新知識的理解和全局觀又進了一步,並且能深刻記住這些知識。
  3. 從點到線到面 學習,有些學習例如源碼,我原以爲我會從一個大的框架開始解讀源碼,我發現我錯得很離譜,那些大而廣的知識只適合和外行吹牛,而對你真正使用的時候幫助不是很大,而且解讀源碼將會非常頭痛,讓人更容易放棄深入學習。

正文

源碼解讀是對自己學習的一個檢驗,希望自己能學到的同時也能幫助更多的人理解相應的知識,然後反哺自己學習的短板。那我們開始學習吧!

JDBC的執行過程

我們知道,mybatis執行最終還是還是在使用jdbc的執行,同時當做複習,我們應該也知道jdbc的執行過程:
在這裏插入圖片描述
在這裏插入圖片描述在這裏插入圖片描述
我們使用jdbc時一共執行的五步:
1.獲取connection
2.預編譯.prepareStatment
3.execute執行結果
4.獲取返回值
5.close連接

JDBC三種執行器

在這裏插入圖片描述

1.statement簡單執行器

基本功能:執行靜態的SQL
傳輸相關:批處理,設置加載的行數

    @Test
    public void statementBatchTest() throws SQLException {
        String sql = "INSERT INTO `db_user` (`name`,`nickname`,`status`,`password`) VALUES ('toto','ynwrd',1,'md5');";
        Statement statement = connection.createStatement();
//        statement.setFetchSize(20);//一次可以讀取多少行結果
        long nowTime = System.currentTimeMillis();
        for (int i = 0; i < 20; i++) {
//            statement.execute(); //單條執行
            statement.addBatch(sql); //添加進批量量,相當於填充進彈藥
//            statement.addBatch(); // 添加批處理參數
        }
        statement.executeBatch(); // 一次全部提交
        System.out.println(System.currentTimeMillis() - nowTime); //批處理效率明顯高於單條執行
        statement.close();
    }

2.preparedStatement預處理執行器

prepardStatement除了簡單執行器的功能,還包括對參數進行預編譯,可以有效的防止SQL注入
能注入的例子:

// sql注入測試
    public int selectListByName(String name) throws SQLException {
        //傳入參數爲 admin' or '1'='1 時就會有SQL注入的問題,能直接拉全庫,
        // 甚至改動參數能刪除數據庫或者拉下其它表的數據,造成不可預估的危險
        String sql = "SELECT * FROM db_user WHERE `name`='" + name + "'";
        System.out.println(sql);
        Statement statement = connection.createStatement();
        statement.executeQuery(sql);
        ResultSet resultSet = statement.getResultSet();
        int count=0;
        while (resultSet.next()){
            count++;
        }
        statement.close();
        return count;
    }

使用prepardStatement 處理器後有效防止注入的例子:

 public int selectListByName2(String name) throws SQLException {
        // 參數再怎麼變也沒用,有效防止sql注入
        String sql = "SELECT * FROM db_user WHERE `name`=?";
        PreparedStatement statement = connection.prepareStatement(sql);
        statement.setString(1,name);
        System.out.println(statement);
        statement.executeQuery();
        ResultSet resultSet = statement.getResultSet();
        int count=0;
        while (resultSet.next()){
            count++;
        }
        statement.close();
        return count;
    }

prepardStatement 批量提交的例子:

    @Test
    public void prepareBatchTest() throws SQLException {
        String sql = "INSERT INTO `db_user` (`name`,`nickname`,`status`,`password`) VALUES ('toto','ynwrd',1,?);";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setFetchSize(20);//一次可以讀取多少行結果

        long nowTime = System.currentTimeMillis();
        for (int i = 0; i < 20; i++) {
            preparedStatement.setString(1, i+"號player");
//            preparedStatement.execute(); //單條執行
            preparedStatement.addBatch(); //添加批處理參數
//            preparedStatement.addBatch(sql); // 添加進批量,如果要用這個則必須是靜態SQL
        }
        preparedStatement.executeBatch(); // 一次全部提交
        System.out.println(System.currentTimeMillis() - nowTime); //批處理效率明顯高於單條執行
        preparedStatement.close();
    }

簡單執行器和預編譯執行器對比圖例:
在這裏插入圖片描述
批量提交時圖例:
在這裏插入圖片描述

3.存儲過程處理器CallableStatement

能設置出參,讀取出參

mybatis執行過程

從現在開始說mybatis了,我認爲步子要一步一步走,飯要一口一口的喫,由於mapper映射和出參設置那些都比較難,我們先看大框架,再從小螺絲一個一個擰。

sqlsession:sql會話,我們所有的mybatis都要通過它來調用,它擁有的功能包括基本的增刪改查,還有輔助功能包括提交和關閉會話。
Executor:執行器,除了提供基本的改、查、緩存維護,還有輔助功能包括提交、關閉執行器、批處理刷新
StatementHandler:聲明處理器,執行器調用聲明處理器並且做參數處理結果處理
後面在總結和描述中會用更加細粒度來解釋這些詞語,如果沒有基礎,你現在強行記會比較費力,我只是在帶入後面需要說的部分

我們看一下sqlsession給我們的查詢方法:
在這裏插入圖片描述

我們看到sqlsession中有很多名字的方法重用,這是一種門面模式
select(String,Object,RowBounds,ResultHandler)
String:statementId,即mapper中的方法的全路徑+方法名:例如:com.toto.UserMapper#selectByid
Object: 傳入的參數
RowBounds:分頁類,可以用來做分頁的類,默認值分頁的大小是 Integer.MAX_VALUE
ResultHandler:結果集處理對象

注意:我們的會話一次可能會對應多條SQL,所以我們的會話不是線程安全的,一個會話只能由一個線程來控制,同時,我們的會話對應了一個執行器,所以我們的會話下的執行器和聲明處理器都不能跨線程使用。一個會話對應一個執行器,一條sql對應一個聲明處理器,對應比例是1:1:n(sql數量)。這些知識可能難以消化,但經過後面的學習會逐步理解整個流程。

在這裏插入圖片描述
根據這個體系,我們一個體系一個體系來講,由於篇幅較長,本文只介紹執行器的體系

Executor執行器體系

  //源代碼路徑:org.apache.ibatis.session.defaults.selectList 
  //門面模式最典型就是不管擴充了多少,最終都會用最多參數的那個類,本文以selectList來追溯源碼
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

我們看到會話執行的查詢最終使用的是executor執行器,同時它的構造方法也有傳入執行器的類型,那我們應該選擇哪種執行器呢?執行器中又有哪些故事呢?它們不同的特別又是什麼呢?讓我們帶着問題進入執行器的探究。

    @Test
    public void sessionTest(){
        //ExecutorType是一個枚舉類,它有SIMPLE、REUSE、BATCH
        SqlSession sqlSession = factory.openSession(ExecutorType.SIMPLE,true);
        // 降低調用複雜性
        List<Object> list = sqlSession.selectList("com.toto.UserMapper.selectByid", 1);
        System.out.println(list.get(0));
    }

然後我們Debug來跑程序,隨後又發現了不同的執行器
在這裏插入圖片描述
當我們斷點繼續往下走,我們進了CachingExecutor,它好像沒有真的執行器實現,彷彿只是依靠傳入的executor來做事情:
在這裏插入圖片描述
想起來了!這是一個裝飾者模式:最輕簡的實現方式,通過獲取實例,來切面完成基礎功能和追加的功能。後面我們再細講這個裝飾者模式,隨後Debug繼續深入,我們看到它又調用了一個query方法,也就是追加了一部分功能,這些功能後面我寫二級緩存時會講解到,然後使用delegate來繼續調用query方法,然後我們深入到了BaseExecutor,原來SimpleExecutor繼承了BaseExecutor,隨後BaseExecutor轉向後又調用了doQuery方法,最後纔到我們的SinpleExecutor。總結一下,我們發現了一共5個執行器,我先畫出來關係圖,隨後逐個講解這些執行器能做哪些事情吧。。

//源碼位置在org.apache.ibatis.executor的322行
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // doQuery方法是抽象方法,由SimpleExecutor、ReuseExecutor、BatchExecutor 來實現
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

在這裏插入圖片描述

SimpleExecutor 簡單執行器

不緩存,有多少條SQL執行多少條

    // 簡單執行器測試
    @Test
    public void simpleTest() throws SQLException {
        SimpleExecutor executor = new SimpleExecutor(configuration, jdbcTransaction);
        ms = configuration.getMappedStatement("com.toto.UserMapper.selectByid");
        List<Object> list = executor.doQuery(ms, 1, RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
        executor.doQuery(ms, 1, RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
        System.out.println(list.get(0));
    }

Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, ynwrd, 2016-08-07 14:07:10
Total: 1
Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, ynwrd, 2016-08-07 14:07:10
Total: 1

根據日誌顯示運行了兩次doQuery打印了兩遍日誌

ReuseExecutor 重用執行器

同樣的SQL(不管是不是同一個statementId,只要SQL一樣,那麼就會重用),並且緩存週期是這個會話

    // 重用執行器
    @Test
    public void ReuseTest() throws SQLException {
        ReuseExecutor executor = new ReuseExecutor(configuration, jdbcTransaction);
        List<Object> list = executor.doQuery(ms, 1, RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
        //  相同的SQL 會緩存對應的 PrepareStatement 它的緩存週期:這個會話
        executor.doQuery(ms, 10, RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
        System.out.println(list.get(0));
    }

Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, ynwrd, 2016-08-07 14:07:10
Total: 1
Parameters: 10(Integer)
Total: 0

根據日誌顯示運行了兩次doQuery只打印了一遍SQL的日誌,但兩次參數都打印了。

BatchExecutor 批處理執行器

批處理執行器一般用在修改SQL的場景時使用,將對SQL進行一次性打包插入,使用這個性能比SimpleExecutor性能要高一些的,但一定要執行doFlushStatements才能生效

    // 批處理執行器
    @Test
    public void BatchTest() throws SQLException {
        BatchExecutor executor = new BatchExecutor(configuration, jdbcTransaction);
        MappedStatement setName = configuration
                .getMappedStatement("com.toto.UserMapper.setNickName");
        Map param = new HashMap();
        param.put("arg0", 1);
        param.put("arg1", "管理員大哥");
        executor.doUpdate(setName, param); //修改的第一條sql
        executor.doUpdate(setName, param);// 修改的第二條sql
        executor.doFlushStatements(false);
    }

==> Preparing: update db_user set nickname=? where id=?
==> Parameters: 管理員大哥(String), 1(Integer)
==> Parameters: 管理員大哥(String), 1(Integer)

能將數據批量新增或者修改,如果遇到需要批量新增或者修改時,可以使用這種執行器,效率將得到極大提升

CachingExecutor

注意:1.二級緩存只有提交後才能使用 2.Bean開啓緩存需要實例化 3.需要開啓二級緩存,如:@CacheNamespace
錄了一個gif,使用commit後能獲取到緩存的圖片
在這裏插入圖片描述
代碼流程:commit以後就提交到二級緩存上,就可以允許被其它會話使用。二級緩存我們後面再開篇幅詳細講解,因爲緩存也存在很多級,東西比較多
代碼示例:

    @Test
    public void cacheExecutorTest() throws SQLException {
        // BaseExecutor
        Executor executor = new SimpleExecutor(configuration,jdbcTransaction);
        // 裝飾器模式
        Executor cachingExecutor=new CachingExecutor(executor);// 二級緩存相關邏輯 執行數據操作邏輯

        cachingExecutor.query(ms, 1, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
        cachingExecutor.commit(true);
        //  提交之後纔會更新
        cachingExecutor.query(ms, 1, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
    }

輸出SQL時:

Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, 管理員大哥, 2016-08-07 14:07:10
Total: 1
Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@130520d]
Cache Hit Ratio [com.toto.UserMapper]: 0.5

緩存命中率0.5,因爲我們只跑了兩次,還有一次存儲緩存,一次命中緩存

Mybaits生成CachingExecutor執行流程

關於spring與mybatis的故事我們後面再聊,在這裏我們先簡單說一下我們生成的流程:
1.SqlSessionFactory.openSession
在這裏插入圖片描述
2.Configuration.newExecutor()
在這裏插入圖片描述
3.構建Executor:例如SimpleExecutor
4.包裝CacheExecutor
在這裏插入圖片描述

Executor執行過程

我這裏簡單展示一下查詢的執行過程,也將分爲四步,debug跑
1.查詢開始
在這裏插入圖片描述
2.sqlSession有執行器的所有功能,因爲它手上拿着執行器:
在這裏插入圖片描述
當沒能觸發二級緩存時(二級緩存應用程序不倒,緩存就不會被GC,除非調用清空方法,後續有講解),就會往下繼續調用:
在這裏插入圖片描述
3.BaseExecutor執行query方法,處理1級緩存和相關邏輯
在這裏插入圖片描述
4.BaseExecutor執行到實際查詢時:
在這裏插入圖片描述
在這裏插入圖片描述

本篇結尾

由於篇幅較長,第一篇解析就到這裏,請期待更多後續,也希望大家多實踐多總結,這樣東西才能學到並掌握

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章