徹底搞懂MyBatis插件原理及PageHelper原理

2020-12-25 我是IT老哥

MyBatis插件實現原理—目錄

前言MyBatis中插件是如何實現的MyBatis插件的使用MyBatis插件實現原理插件的加載插件如何進行攔截攔截Executor對象其他對象插件解析插件執行流程假如一個對象被代理很多次PageHelper插件的使用PageHelper插件原理為什麼PageHelper只對startPage後的第一條select語句有效不通過插件能否改變MyBatis的核心行為總結前言

提到插件,相信大家都知道,插件的存在主要是用來改變或者增強原有的功能,MyBatis中也一樣。

然而如果我們對MyBatis的工作原理不是很清楚的話,最好不要輕易使用插件,否則的話如果因為使用插件導致了底層工作邏輯被改變,很可能會出現很多意料之外的問題。

本文主要會介紹MyBatis插件的使用及其實現原理,相信讀完本文,我們也可以寫出自己的PageHelper分頁插件了。

MyBatis中插件是如何實現的

在MyBatis中插件式通過攔截器來實現的,那麼既然是通過攔截器來實現的,就會有一個問題,哪些對象才允許被攔截呢?

真正執行Sql的是四大對象:Executor,StatementHandler,ParameterHandler,ResultSetHandler。

而MyBatis的插件正是基於攔截這四大對象來實現的。需要注意的是,雖然我們可以攔截這四大對象,但是並不是這四大對象中的所有方法都能被攔截,下面就是官網提供的可攔截的對象和方法匯總:

在這裡插入圖片描述MyBatis插件的使用

首先我們先來通過一個例子來看看如何使用插件。

1、首先建立一個MyPlugin實現接口Interceptor,然後重寫其中的三個方法(注意,這裡必須要實現Interceptor接口,否則無法被攔截)。

package com.lonelyWolf.mybatis.plugin;import org.apache.ibatis.executor.Executor;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.plugin.*;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.RowBounds;import java.util.Properties;@Intercepts({@Signature(type = Executor.class,method= "query",args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})})publicclassMyPluginimplementsInterceptor{/** * 這個方法會直接覆蓋原有方法 * @param invocation * @return * @throws Throwable */@Overridepublic Object intercept(Invocation invocation)throws Throwable { System.out.println("成功攔截了Executor的query方法,在這裡我可以做點什麼");return invocation.proceed();//調用原方法 }@Overridepublic Object plugin(Object target){return Plugin.wrap(target,this);//把被攔截對象生成一個代理對象 }@OverridepublicvoidsetProperties(Properties properties){//可以自定義一些屬性 System.out.println("自定義屬性:userName->" + properties.getProperty("userName")); }}@Intercepts是聲明當前類是一個攔截器,後面的@Signature是標識需要攔截的方法籤名,通過以下三個參數來確定

type:被攔截的類名method:被攔截的方法名args:標註方法的參數類型2、 我們還需要在mybatis-config中配置好插件。

<plugins><plugininterceptor="com.lonelyWolf.mybatis.plugin.MyPlugin"><propertyname="userName"value="張三"/></plugin></plugins>這裡如果配置了property屬性,那麼我們可以在setProperties獲取到。

完成以上兩步,我們就完成了一個插件的配置了,接下來我們運行一下:

可以看到,setProperties方法在加載配置文件階段就會被執行了。

MyBatis插件實現原理

接下來讓我們分析一下從插件的加載到初始化到運行整個過程的實現原理。

插件的加載

既然插件需要在配置文件中進行配置,那麼肯定就需要進行解析,我們看看插件式如何被解析的。我們進入XMLConfigBuilder類看看

在這裡插入圖片描述解析出來之後會將插件存入InterceptorChain對象的list屬性

在這裡插入圖片描述看到InterceptorChain我們是不是可以聯想到,MyBatis的插件就是通過責任鏈模式實現的

插件如何進行攔截

既然插件類已經被加載到配置文件了,那麼接下來就有一個問題了,插件類何時會被攔截我們需要攔截的對象呢?

其實插件的攔截是和對象有關的,不同的對象進行攔截的時間也會不一致,接下來我們就逐一分析一下。

攔截Executor對象

我們知道,SqlSession對象是通過openSession()方法返回的,而Executor又是屬於SqlSession內部對象,所以讓我們跟隨openSession方法去看一下Executor對象的初始化過程。

在這裡插入圖片描述可以看到,當初始化完成Executor之後,會調用interceptorChain的pluginAll方法,pluginAll方法本身非常簡單,就是把我們存到list中的插件進行循環,並調用Interceptor對象的plugin方法:

在這裡插入圖片描述再次點擊進去:

在這裡插入圖片描述到這裡我們是不是發現很熟悉,沒錯,這就是我們上面示例中重寫的方法,而plugin方法是接口中的一個默認方法。

這個方法是關鍵,我們進去看看:

在這裡插入圖片描述可以看到這個方法的邏輯也很簡單,但是需要注意的是MyBatis插件是通過JDK動態代理來實現的。

而JDK動態代理的條件就是被代理對象必須要有接口,這一點和Spring中不太一樣,Spring中是如果有接口就採用JDK動態代理,沒有接口就是用CGLIB動態代理。

正因為MyBatis的插件只使用了JDK動態代理,所以我們上面才強調了一定要實現Interceptor接口。

而代理之後匯之星Plugin的invoke方法,我們最後再來看看invoke方法:

在這裡插入圖片描述而最終執行的intercept方法,就是我們上面示例中重寫的方法。

其他對象插件解析

接下來我們再看看StatementHandler,StatementHandler是在Executor中的doQuery方法創建的,其實這個原理就是一樣的了,找到初始化StatementHandler對象的方法:

在這裡插入圖片描述進去之后里面執行的也是pluginAll方法:

在這裡插入圖片描述其他兩個對象就不在舉例了,其實搜一下全局就很明顯了:

在這裡插入圖片描述四個對象初始化的時候都會調用pluginAll來進行判定是否有被代理。

插件執行流程

下面就是實現了插件之後的執行時序圖:

在這裡插入圖片描述假如一個對象被代理很多次

一個對象是否可以被多個代理對象進行代理?也就是說同一個對象的同一個方法是否可以被多個攔截器進行攔截?

答案是肯定的,因為被代理對象是被加入到list,所以我們配置在最前面的攔截器最先被代理,但是執行的時候卻是最外層的先執行

具體點:

假如依次定義了三個插件:插件A,插件B 和 插件C。

那麼List中就會按順序存儲:插件A,插件B 和 插件C。

而解析的時候是遍歷list,所以解析的時候也是按照:插件A,插件B 和 插件C的順序。

但是執行的時候就要反過來了,執行的時候是按照:插件C,插件B和插件A的順序進行執行。

PageHelper插件的使用

上面我們了解了在MyBatis中的插件是如何定義以及MyBatis中是如何處理插件的,接下來我們就以經典分頁插件PageHelper為例來進一步加深理解。

首先我們看看PageHelper的用法:

package com.lonelyWolf.mybatis;import com.alibaba.fastjson.JSONObject;import com.github.pagehelper.Page;import com.github.pagehelper.PageHelper;import com.github.pagehelper.PageInfo;import com.lonelyWolf.mybatis.mapper.UserMapper;import com.lonelyWolf.mybatis.model.LwUser;import org.apache.ibatis.executor.result.DefaultResultHandler;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.ResultHandler;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import java.io.IOException;import java.io.InputStream;import java.util.List;publicclassMyBatisByPageHelp{publicstaticvoidmain(String[] args)throws IOException { String resource = "mybatis-config.xml";//讀取mybatis-config配置文件 InputStream inputStream = Resources.getResourceAsStream(resource);//創建SqlSessionFactory對象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//創建SqlSession對象 SqlSession session = sqlSessionFactory.openSession(); PageHelper.startPage(0,10); UserMapper userMapper = session.getMapper(UserMapper.class); List<LwUser> userList = userMapper.listAllUser(); PageInfo<LwUser> pageList = new PageInfo<>(userList); System.out.println(null == pageList ? "": JSONObject.toJSONString(pageList)); }}輸出如下結果:

在這裡插入圖片描述可以看到對象已經被分頁,那麼這是如何做到的呢?

PageHelper插件原理

我們上面提到,要實現插件必須要實現MyBatis提供的Interceptor接口,所以我們去找一下,發現PageHeler實現了Interceptor:

在這裡插入圖片描述經過上面的介紹這個類應該一眼就能看懂,我們關鍵要看看SqlUtil的intercept方法做了什麼:

在這裡插入圖片描述這個方法的邏輯比較多,因為要考慮到不同的資料庫方言的問題,所以會有很多判斷,我們主要是關注PageHelper在哪裡改寫了sql語句,上圖中的紅框就是改寫了sql語句的地方:

在這裡插入圖片描述這裡面會獲取到一個Page對象,然後在愛寫sql的時候也會將一些分頁參數設置到Page對象,我們看看Page對象是從哪裡獲取的:

在這裡插入圖片描述我們看到對象是從LOCAL_PAGE對象中獲取的,這個又是什麼呢?

在這裡插入圖片描述這是一個本地線程池變量,那麼這裡面的Page又是什麼時候存進去的呢?這就要回到我們的示例上了,分頁的開始必須要調用:

PageHelper.startPage(0,10);

在這裡插入圖片描述這裡就會構建一個Page對象,並設置到ThreadLocal內。

為什麼PageHelper只對startPage後的第一條select語句有效

這個其實也很簡單哈,但是可能會有人有這個以為,我們還是要回到上面的intercept方法:

在這裡插入圖片描述在finally內把ThreadLocal中的分頁數據給清除掉了,所以只要執行一次查詢語句就會清除分頁信息,故而後面的select語句自然就無效了。

不通過插件能否改變MyBatis的核心行為

上面我們介紹了通過插件來改變MyBatis的核心行為,那麼不通過插件是否也可以實現呢?

答案是肯定的,官網中提到,我們可以通過覆蓋配置類來實現改變MyBatis核心行為,也就是我們自己寫一個類繼承Configuration類,然後實現其中的方法,最後構建SqlSessionFactory對象的時候傳入自定義的Configuration方法:

SqlSessionFactory build(MyConfiguration)當然,這種方法是非常不建議使用的,因為這種方式就相當於在建房子的時候把地基抽出來重新建了,稍有不慎,房子就要塌了。

總結

本文主要會介紹MyBatis插件的使用及MyBatis其實現原理,最後我們也大致介紹了PageHelper插件的主要實現原理,相信讀完本文學會MyBatis插件原理之後,我們也可以寫個簡單的自己的PageHelper分頁插件了。

源於:https://blog.csdn.net/zwx900102/article/details/108941441

相關焦點

  • 建議收藏,mybatis插件原理詳解
    上次發文說到了如何集成分頁插件MyBatis插件原理分析,看完感覺自己better了,今天我們接著來聊mybatis插件的原理。插件原理分析mybatis插件涉及到的幾個類:我將以 Executor 為例,分析 MyBatis 是如何為 Executor 實例植入插件的。Executor 實例是在開啟 SqlSession 是被創建的,因此,我們從源頭進行分析。
  • pageHelper分頁失效解決方案
    前言      pageHelper是一款優秀的Mybatis分頁插件,在項目中可以非常便利的使用,使開發效率得到很大的提升,但不支持一對多結果映射的分頁查詢
  • 牛逼哄哄的PageHelper分頁插件到底是怎麼實現的?網友:給我10分鐘,給你寫一個~
    原本以為分頁插件,應該是很簡單的,然而PageHelper比我想像的要複雜許多,它做的很強大,也很徹底,強大到使用者可能並不需要這麼多功能,徹底到一參可以兩用。但是,我認為,作為分頁插件,完成物理分頁任務是根本,其它的很多智能並不是必要的,保持它夠傻夠憨,專業術語叫stupid,簡單就是美。
  • Mybatis 分頁插件 3.7.2 發布 - OSCHINA - 中文開源技術交流社區
    Mybatis分頁插件 - PageHelper 如果你也在用Mybatis,建議嘗試該分頁插件,這一定是
  • Mybatis 分頁插件 3.7.3 發布
    >最方便使用的分頁插件。分頁插件支持任何複雜的單表、多表分頁,部分特殊情況請看重要提示。想要使用分頁插件?DB2SqlServer(2005+)Informix3.7.3更新日誌:Page繼承的ArrayList,原先會根據pageSize初始化大小,這就導致當pageSize過大(如Integer.MAX_VALUE)時的內存溢出(實際數據量很小
  • Spring Boot 2.X 實戰--SQL 資料庫(MyBatis)
    新建項目2.1、分頁插件 Pagehelper對於 MyBatis 的分頁,在本小節中,通過 Pagehelper 來實現。:2.1.1' 4    implementation 'com.github.pagehelper:pagehelper-spring-boot-starter:1.2.13' 5    compileOnly 'org.projectlombok:lombok' 6    runtimeOnly 'mysql:mysql-connector-java' 7
  • 深入理解 Mybatis 插件開發
    分頁功能mybatis的分頁默認是基於內存分頁的(查出所有,再截取),數據量大的情況下效率較低,不過使用mybatis插件可以改變該行為,只需要攔截StatementHandler類的prepare方法,改變要執行的SQL語句為分頁語句即可;公共欄位統一賦值一般業務系統都會有創建者,創建時間,修改者,修改時間四個欄位
  • Mybatis的分頁插件PageHelper源碼解析和性能優化
    1、Maven中引入依賴2、代碼中分頁的使用3、源碼分析PageHelper.startPage(page,pageSize);
  • 從Mybatis源碼分心詳解你不知道的Mybatis用法和細節
    分頁不需要插件RowBounds本系列使用篇中提到使用 pagehelper 來支持分頁功能,本質上是使用了插件對 sql 植入分頁參數。其實,Mybatis 已經提供了RowBounds這類來支持分頁功能,這種方式不需要安裝插件,MybatisPlus 本質上就是使用了這種方式。
  • 匯總一下Intellij IDEA炫酷的插件
    3、彩色括號 Rainbow Brackets4、mybatis插件集合 :MyBatis Log Plugin MyBatisCodeHelperPro Free Mybatis plugin推薦指數:☆☆☆☆☆
  • Java程式設計師必須知道的MyBatis經典面試題
    mybatis把sql語句從Java代碼抽離出來,單獨放在XML文件管理,管理非常方便mybatis底層封裝了JDBC接口,並自動將結果映射到Java bean中,大大減少代碼的編寫數量,減少重複的工作量mybatis可以手動編寫SQL語句,靈活控制SQL語句,編寫更好的
  • mybatis-plus 3.0-alpha 發布,代號:超級棒棒糖
    mybatis-plus 3.0-alpha 發布,代號:超級棒棒糖Mybatis-Plus 是一款 Mybatis
  • 10道MyBatis面試題,你值得看看
    可以在sql內直接書寫帶有物理分頁的參數來完成物理分頁功能,也可以使用分頁插件來完成物理分頁。7.分頁插件的基本原理是什麼?分頁插件的基本原理是使用Mybatis提供的插件接口,實現自定義插件,在插件的攔截方法內攔截待執行的sql,然後重寫sql(SQL拼接limit),根據dialect方言,添加對應的物理分頁語句和物理分頁參數,用到了技術JDK動態代理,用到了責任鏈設計模式。8.簡述Mybatis的插件運行原理?
  • MyBatis-Plus為啥這麼牛?
    Mapper 、 Model 、 Service 、 Controller 層代碼,支持模板引擎,更有超多自定義配置等您來使用內置分頁插件:基於 MyBatis 物理分頁,開發者無需關心具體操作,配置好插件之後,寫分頁等同於普通 List 查詢分頁插件支持多種資料庫:支持 MySQL、MariaDB
  • Mybatis-Generator,讓你的懶體現到極致!
    一 mybatis-generator介紹在我們對用mybatis對資料庫進行操作的時候,需要編寫實體類、接口和接口映射文件等...雖然這些是一些比較簡單的工作,但是如果操作的表一旦多起來也是比較煩心的一件事,而且是一些無腦工作。現在,有一款mybatis-generation插件可以幫我們自動完成這些事情,只需要簡單的配置既可。
  • 企業面試求職中關於mybatis必問的18道面試題解答!
    Mapper接口是沒有實現類的,當調用接口方法時,接口全限名+方法名拼接字符串作為key值,可唯一定位一個MappedStatement舉例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace為com.mybatis3.mappers.StudentDao下面id = findStudentById
  • 徹底理解Java並發編程原理!
    具體包括線程的狀態、Java線程調度策略、線程優先級、並發模型、悲觀鎖樂觀鎖、JDK各種同步器、JDK內置AQS同步器、線程與IO、Java線程與JVM線程、阻塞與喚醒機制、JDK並發包各種工具剖析、自旋、JDK內置並發鎖、CAS、synchronized、線程池、線程之間的協作等並發方面知識及原理進行深入淺出的講解。
  • 你必須知道的 webpack 插件原理分析
    Webpack 的 Tapable 事件流機制保證了插件的有序性,將各個插件串聯起來, Webpack 在運行過程中會廣播事件,插件只需要監聽它所關心的事件,就能加入到這條 webapck 機制中,去改變 webapck 的運作,使得整個系統擴展性良好。
  • Mybatis初始化過程簡單總結
    前面連續多篇文章都是在數據mybatis的初始化過程,目前基本完成,是時候做一個總結了。上圖中只畫出了SqlSessionFactory初始化過程以及mapper的加載過程,由於其他比如Configuration的屬性、別名、插件、數據源配置、類型映射處理器的初始化過程比較簡單並沒有囊括進去。