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特性會失效嗎?)
源代碼追溯:
(未完待續。。。)
應用開發之二、 樂觀鎖使用攔截器
(未完待續。。。)