只要五分鐘,徹底搞懂MyBatis插件原理及PageHelper原理
點(diǎn)擊上方“程序員大白”,選擇“星標(biāo)”公眾號
重磅干貨,第一時(shí)間送達(dá)
前言
提到插件,相信大家都知道,插件的存在主要是用來改變或者增強(qiáng)原有的功能,MyBatis中也一樣。然而如果我們對MyBatis的工作原理不是很清楚的話,最好不要輕易使用插件,否則的話如果因?yàn)槭褂貌寮?dǎo)致了底層工作邏輯被改變,很可能會出現(xiàn)很多意料之外的問題。
本文主要會介紹MyBatis插件的使用及其實(shí)現(xiàn)原理,相信讀完本文,我們也可以寫出自己的PageHelper分頁插件了。
MyBatis中插件是如何實(shí)現(xiàn)的
在MyBatis中插件式通過攔截器來實(shí)現(xiàn)的,那么既然是通過攔截器來實(shí)現(xiàn)的,就會有一個(gè)問題,哪些對象才允許被攔截呢?
回想前面我們介紹Executor的文章中提到,真正執(zhí)行Sql的是四大對象:Executor,StatementHandler,ParameterHandler,ResultSetHandler。而MyBatis的插件正是基于攔截這四大對象來實(shí)現(xiàn)的。
需要注意的是,雖然我們可以攔截這四大對象,但是并不是這四大對象中的所有方法都能被攔截,下面就是官網(wǎng)提供的可攔截的對象和方法匯總:

MyBatis插件的使用
首先我們先來通過一個(gè)例子來看看如何使用插件。
1、首先建立一個(gè)MyPlugin實(shí)現(xiàn)接口Interceptor,然后重寫其中的三個(gè)方法(注意,這里必須要實(shí)現(xiàn)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})})
public?class?MyPlugin?implements?Interceptor?{
????/**
?????*?這個(gè)方法會直接覆蓋原有方法
?????*?@param?invocation
?????*?@return
?????*?@throws?Throwable
?????*/
????@Override
????public?Object?intercept(Invocation?invocation)?throws?Throwable?{
????????System.out.println("成功攔截了Executor的query方法,在這里我可以做點(diǎn)什么");
????????return?invocation.proceed();//調(diào)用原方法
????}
????@Override
????public?Object?plugin(Object?target)?{
????????return?Plugin.wrap(target,this);//把被攔截對象生成一個(gè)代理對象
????}
????@Override
????public?void?setProperties(Properties?properties)?{//可以自定義一些屬性
????????System.out.println("自定義屬性:userName->"?+?properties.getProperty("userName"));
????}
}
@Intercepts是聲明當(dāng)前類是一個(gè)攔截器,后面的@Signature是標(biāo)識需要攔截的方法簽名,通過以下三個(gè)參數(shù)來確定
type:被攔截的類名。 method:被攔截的方法名 args:標(biāo)注方法的參數(shù)類型
2、我們還需要在mybatis-config中配置好插件。
<plugins>
????<plugin?interceptor="com.lonelyWolf.mybatis.plugin.MyPlugin">
??????<property?name="userName"?value="張三"/>
????plugin>
plugins>
這里如果配置了property屬性,那么我們可以在setProperties獲取到。
完成以上兩步,我們就完成了一個(gè)插件的配置了,接下來我們運(yùn)行一下:

可以看到,setProperties方法在加載配置文件階段就會被執(zhí)行了。
MyBatis插件實(shí)現(xiàn)原理
接下來讓我們分析一下從插件的加載到初始化到運(yùn)行整個(gè)過程的實(shí)現(xiàn)原理。
插件的加載
既然插件需要在配置文件中進(jìn)行配置,那么肯定就需要進(jìn)行解析,我們看看插件式如何被解析的。我們進(jìn)入XMLConfigBuilder類看看

解析出來之后會將插件存入InterceptorChain對象的list屬性

看到InterceptorChain我們是不是可以聯(lián)想到,MyBatis的插件就是通過責(zé)任鏈模式實(shí)現(xiàn)的。
插件如何進(jìn)行攔截
既然插件類已經(jīng)被加載到配置文件了,那么接下來就有一個(gè)問題了,插件類何時(shí)會被攔截我們需要攔截的對象呢?
其實(shí)插件的攔截是和對象有關(guān)的,不同的對象進(jìn)行攔截的時(shí)間也會不一致,接下來我們就逐一分析一下。
攔截Executor對象
我們知道,SqlSession對象是通過openSession()方法返回的,而Executor又是屬于SqlSession內(nèi)部對象,所以讓我們跟隨openSession方法去看一下Executor對象的初始化過程。

可以看到,當(dāng)初始化完成Executor之后,會調(diào)用interceptorChain的pluginAll方法,pluginAll方法本身非常簡單,就是把我們存到list中的插件進(jìn)行循環(huán),并調(diào)用Interceptor對象的plugin方法:

再次點(diǎn)擊進(jìn)去:

到這里我們是不是發(fā)現(xiàn)很熟悉,沒錯(cuò),這就是我們上面示例中重寫的方法,而plugin方法是接口中的一個(gè)默認(rèn)方法。
這個(gè)方法是關(guān)鍵,我們進(jìn)去看看:

可以看到這個(gè)方法的邏輯也很簡單,但是需要注意的是MyBatis插件是通過JDK動(dòng)態(tài)代理來實(shí)現(xiàn)的,而JDK動(dòng)態(tài)代理的條件就是被代理對象必須要有接口,這一點(diǎn)和Spring中不太一樣,Spring中是如果有接口就采用JDK動(dòng)態(tài)代理,沒有接口就是用CGLIB動(dòng)態(tài)代理。
正因?yàn)镸yBatis的插件只使用了JDK動(dòng)態(tài)代理,所以我們上面才強(qiáng)調(diào)了一定要實(shí)現(xiàn)Interceptor接口。
而代理之后匯之星Plugin的invoke方法,我們最后再來看看invoke方法:

而最終執(zhí)行的intercept方法,就是我們上面示例中重寫的方法。
其他對象插件解析
接下來我們再看看StatementHandler,StatementHandler是在Executor中的doQuery方法創(chuàng)建的,其實(shí)這個(gè)原理就是一樣的了,找到初始化StatementHandler對象的方法:

進(jìn)去之后里面執(zhí)行的也是pluginAll方法:

其他兩個(gè)對象就不在舉例了,其實(shí)搜一下全局就很明顯了:
PS:

四個(gè)對象初始化的時(shí)候都會調(diào)用pluginAll來進(jìn)行判定是否有被代理。
插件執(zhí)行流程
下面就是實(shí)現(xiàn)了插件之后的執(zhí)行時(shí)序圖:

假如一個(gè)對象被代理很多次
一個(gè)對象是否可以被多個(gè)代理對象進(jìn)行代理?也就是說同一個(gè)對象的同一個(gè)方法是否可以被多個(gè)攔截器進(jìn)行攔截?
答案是肯定的,因?yàn)楸淮韺ο笫潜患尤氲絣ist,所以我們配置在最前面的攔截器最先被代理,但是執(zhí)行的時(shí)候卻是最外層的先執(zhí)行。
具體點(diǎn):
假如依次定義了三個(gè)插件:插件A,插件B和插件C。
那么List中就會按順序存儲:插件A,插件B和插件C,而解析的時(shí)候是遍歷list,所以解析的時(shí)候也是按照:插件A,插件B和插件C的順序,但是執(zhí)行的時(shí)候就要反過來了,執(zhí)行的時(shí)候是按照:插件C,插件B和插件A的順序進(jìn)行執(zhí)行。
PageHelper插件的使用
上面我們了解了在MyBatis中的插件是如何定義以及MyBatis中是如何處理插件的,接下來我們就以經(jīng)典分頁插件PageHelper為例來進(jìn)一步加深理解。
首先我們看看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;
public?class?MyBatisByPageHelp?{
????public?static?void?main(String[]?args)?throws?IOException?{
????????String?resource?=?"mybatis-config.xml";
????????//讀取mybatis-config配置文件
????????InputStream?inputStream?=?Resources.getResourceAsStream(resource);
????????//創(chuàng)建SqlSessionFactory對象
????????SqlSessionFactory?sqlSessionFactory?=?new?SqlSessionFactoryBuilder().build(inputStream);
????????//創(chuàng)建SqlSession對象
????????SqlSession?session?=?sqlSessionFactory.openSession();
????????PageHelper.startPage(0,10);
????????UserMapper?userMapper?=?session.getMapper(UserMapper.class);
????????List?userList?=?userMapper.listAllUser();
????????PageInfo?pageList?=?new?PageInfo<>(userList);
????????System.out.println(null?==?pageList???"":?JSONObject.toJSONString(pageList));
????}
}
輸出如下結(jié)果:

可以看到對象已經(jīng)被分頁,那么這是如何做到的呢?
PageHelper插件原理
我們上面提到,要實(shí)現(xiàn)插件必須要實(shí)現(xiàn)MyBatis提供的Interceptor接口,所以我們?nèi)フ乙幌?,發(fā)現(xiàn)PageHeler實(shí)現(xiàn)了Interceptor:

經(jīng)過上面的介紹這個(gè)類應(yīng)該一眼就能看懂,我們關(guān)鍵要看看SqlUtil的intercept方法做了什么:

這個(gè)方法的邏輯比較多,因?yàn)橐紤]到不同的數(shù)據(jù)庫方言的問題,所以會有很多判斷,我們主要是關(guān)注PageHelper在哪里改寫了sql語句,上圖中的紅框就是改寫了sql語句的地方:

這里面會獲取到一個(gè)Page對象,然后在愛寫sql的時(shí)候也會將一些分頁參數(shù)設(shè)置到Page對象,我們看看Page對象是從哪里獲取的:

我們看到對象是從LOCAL_PAGE對象中獲取的,這個(gè)又是什么呢?

這是一個(gè)本地線程池變量,那么這里面的Page又是什么時(shí)候存進(jìn)去的呢?
這就要回到我們的示例上了,分頁的開始必須要調(diào)用:
PageHelper.startPage(0,10);

這里就會構(gòu)建一個(gè)Page對象,并設(shè)置到ThreadLocal內(nèi)。
為什么PageHelper只對startPage后的第一條select語句有效
這個(gè)其實(shí)也很簡單哈,但是可能會有人有這個(gè)以為,我們還是要回到上面的intercept方法:

在finally內(nèi)把ThreadLocal中的分頁數(shù)據(jù)給清除掉了,所以只要執(zhí)行一次查詢語句就會清除分頁信息,故而后面的select語句自然就無效了。
不通過插件能否改變MyBatis的核心行為
上面我們介紹了通過插件來改變MyBatis的核心行為,那么不通過插件是否也可以實(shí)現(xiàn)呢?
答案是肯定的,官網(wǎng)中提到,我們可以通過覆蓋配置類來實(shí)現(xiàn)改變MyBatis核心行為,也就是我們自己寫一個(gè)類繼承Configuration類,然后實(shí)現(xiàn)其中的方法,最后構(gòu)建SqlSessionFactory對象的時(shí)候傳入自定義的Configuration方法:
SqlSessionFactory?build(MyConfiguration)
當(dāng)然,這種方法是非常不建議使用的,因?yàn)檫@種方式就相當(dāng)于在建房子的時(shí)候把地基抽出來重新建了,稍有不慎,房子就要塌了。
總結(jié)
本文主要會介紹MyBatis插件的使用及MyBatis其實(shí)現(xiàn)原理,最后我們也大致介紹了PageHelper插件的主要實(shí)現(xiàn)原理,相信讀完本文學(xué)會MyBatis插件原理之后,我們也可以寫個(gè)簡單的自己的PageHelper分頁插件了。
來源:blog.csdn.net/zwx900102/article/
details/108941441
推薦閱讀
關(guān)于程序員大白
程序員大白是一群哈工大,東北大學(xué),西湖大學(xué)和上海交通大學(xué)的碩士博士運(yùn)營維護(hù)的號,大家樂于分享高質(zhì)量文章,喜歡總結(jié)知識,歡迎關(guān)注[程序員大白],大家一起學(xué)習(xí)進(jìn)步!


