定製Mybatis攔截器開發【含源碼追溯】

 1、需求分析

需求:在代碼層面獲得Mybatis執行的SQL,修改SQL,並執行修改後的SQL

方案:Mybatis 攔截器:

 

 

 注意:添加攔截器後,會攔截所有的方法

 思考:其實攔截器就等同於Spring的AOP編程

 細粒度:Mybatis框架中,sql最後都會交給Sqlsession執行,攔截器攔截的其實就是:

  • 1、Executor執行階段
  • 2、ParameterHandler參數處理階段
  • 3、StatementHandler預編譯處理階段
  • 4、ResultSetHandler結果集處理階段 
注:我們在執行Sql之前,需要先獲取Mybatis的SqlSession對象,但框架的SqlSession下面還有四大對象,
所以SqlSession只是個甩手掌櫃,
真正幹活的卻是Executor等四大對象:Executor,StatementHandler,ParameterHandler,ResultSetHandler。

比較形象的畫圖比喻:

 

 

 繼續分析:

理論上來說,如果想攔截,這四個對象的所有方法均可攔截到

但是原則是原則,實際是實際

就像Java中的String類,它裏面這麼多方法,總有幾個常用的

在這邊主要攔截兩個常用對象:

 

 按照Mybatis執行Sql的週期來說,Executor會把sql交給StatementHandler處理,

 所以我們常見的攔截方法還是StatementHandler,而且這個階段的Sql已完成預編譯,佔位符出現,對Sql來說比較全面了(此時除了參數未賦值,其他都全了,就等執行了)

2、熟悉攔截器的開發結構

 1、編碼 

  類實現Intercepyor接口@Override

// 核心攔截邏輯編寫    
public Object intercept(Invocation invocation) throws Throwable { // 編寫攔截邏輯
System.out.println("編寫攔截邏輯");
// 放行該方法 return invocation.proceed(); } // 把這個攔截器的目標傳給下一個攔截器 @Override public Object plugin(Object target) { // 當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標本身,減少目標被代理的次數 if (target instanceof StatementHandler) { System.out.println("Wrapper::"); return Plugin.wrap(target, this); } else { System.out.println("pass on Wrapper::"); return target; } @Override public void setProperties(Properties properties) { //此處可以接收到配置文件的property參數,就像工作流裏面的局部變量,如果業務上需要定義參數,可以通過此方法 // System.out.println(properties.getProperty("name")); }

 

  類 標註攔截的方法和參數(防止方法重載,需參數判別方法的唯一性)

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class AccessControlInterceptor implements Interceptor {

}

 

2、配置(Configure配置)

  在  mybatis-config.xml  中通過plugins 標籤配置攔截器

  或者利用SpringBoot快速定義@Configure標籤標識Bean

思考:如何控制攔截器的執行順序?
         比如我想讓攔截器x執行完之後,再執行攔截器y
參考鏈接:https://juejin.cn/post/6926849748481605646

好的,到這裏。執行到每一個mapper文件的接口對應的sql 都會起作用了。(包括PageHelper 自動生成的Count查詢,同樣會被攔截)

思考:1、如何只攔截SELECT類型的sql,自動過濾掉update類型?
     2、如何攔截指定的mapper接口方法?
         
方案:1、可以通過攔截方法的invocation參數利用反射獲取本次執行sql的 
        MappedStatement,進而獲得sqlCommandType
         
     2、可在mapper接口方法層面添加自定義註解,同樣是通過反射的方式 
        獲得註解標誌位判定應不應該進行攔截,還可通過註解傳遞有含義的 
        value

 

3、更細緻的分析 

爲什麼要攔截StatementHandler的prepare呢

1、Executor 會交給StatementHandler,

    在StatementHandler接口實現類的方法調用中,

    StatementHandler的prepare生產出的Statemen會作爲參數提供給CRUD和批處理之類的操作

    攔截prepare方法是一勞永逸

調用結構順序如下圖:

 

2、攔截prepare方法可以獲得當前的Connection對象,

     Connection對象是所有Java接入所有數據庫的規則接口,可操作性的東西會很多

     sql片段、事務,等同於JDBC  爲所欲爲 

 

 

 4、攔截器應用舉例

一般什麼情況下會使用攔截器:當需要操作sql時

例如:分頁(添加limit sql片段),樂觀鎖(設置版本號校驗,校驗成功直接更新版本和數據,校驗失敗直接提示線程爭搶失敗請重試)

應用開發之一、pagehelper如何使用攔截器實現分頁

 

找一個

   1、com.github.pagehelper.PageInterceptor 攔截請求

 

 

 第 71 - 第99行都是校驗參數、創建緩存之類的操作

   2、憑空捏造Count查詢

第 100 行,pagehelper官方團隊給出的註釋是:查詢總數

 

 

 繼續追代碼,進入count(..)

 

先判斷是否存在手寫的 count 查詢,

存在Count查詢,用原來的 ms ,更新部分參數,直接執行並返回結果,

沒有的話,根據當前的 ms 創建一個返回值爲 Long 類型的 ms,並放到緩存中去

最後執行Count查詢,並返回Count

 

接着做了一個小優化

處理查詢總數,當查詢總數爲 0 時,直接結束查詢

 

   3、修改原始查詢sql 拼接 limit進行分頁

根據上一步查詢來的Count,結合當前頁,計算limit值,並交給Executor去執行query

 

至此,pagehelper整個分頁粗略過程完成

4、思考使用的細節

     PageHelper是如何做到只對緊跟着的第一條SQL起作用的?即使在下面添加再多的select,他仍然只對第一個select情有獨鍾

 

 

 

先揭曉答案: PageHelper.startPage在當前線程中創建一個線程變量 t1,

                   需要的時候就去當前線程獲取 t1,使用完一次就在finally中remove掉 t1

                   受限於代碼同步執行的特性,實現了只對第一個select起作用

                 (思考:若開啓新線程,在主查詢之前執行異步查詢,Pagehelper特性會失效嗎?)

源代碼追溯:

 (未完待續。。。)

 應用開發之二、 樂觀鎖使用攔截器

 (未完待續。。。)

 

 

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