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

MyBatis插件的使用
首先我們先來(lái)通過(guò)一個(gè)例子來(lái)看看如何使用插件。
1、首先建立一個(gè)MyPlugin實(shí)現(xiàn)接口Interceptor,然后重寫(xiě)其中的三個(gè)方法(注意,這里必須要實(shí)現(xiàn)Interceptor接口,否則無(wú)法被攔截)。
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è)方法會(huì)直接覆蓋原有方法
?????*?@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);//把被攔截對(duì)象生成一個(gè)代理對(duì)象
????}
????@Override
????public?void?setProperties(Properties?properties)?{//可以自定義一些屬性
????????System.out.println("自定義屬性:userName->"?+?properties.getProperty("userName"));
????}
}
@Intercepts是聲明當(dāng)前類是一個(gè)攔截器,后面的@Signature是標(biāo)識(shí)需要攔截的方法簽名,通過(guò)以下三個(gè)參數(shù)來(lái)確定
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è)插件的配置了,接下來(lái)我們運(yùn)行一下:

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

解析出來(lái)之后會(huì)將插件存入InterceptorChain對(duì)象的list屬性

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

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

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

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

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

而最終執(zhí)行的intercept方法,就是我們上面示例中重寫(xiě)的方法。
其他對(duì)象插件解析
接下來(lái)我們?cè)倏纯?code style="font-size: 14px;border-radius: 4px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(155, 110, 35);background-color: rgb(255, 245, 227);padding: 3px;margin: 3px;">StatementHandler,StatementHandler是在Executor中的doQuery方法創(chuàng)建的,其實(shí)這個(gè)原理就是一樣的了,找到初始化StatementHandler對(duì)象的方法:

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

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

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

假如一個(gè)對(duì)象被代理很多次
一個(gè)對(duì)象是否可以被多個(gè)代理對(duì)象進(jìn)行代理?也就是說(shuō)同一個(gè)對(duì)象的同一個(gè)方法是否可以被多個(gè)攔截器進(jìn)行攔截?
答案是肯定的,因?yàn)楸淮韺?duì)象是被加入到list,所以我們配置在最前面的攔截器最先被代理,但是執(zhí)行的時(shí)候卻是最外層的先執(zhí)行。
具體點(diǎn):
假如依次定義了三個(gè)插件:插件A,插件B和插件C。
那么List中就會(huì)按順序存儲(chǔ):插件A,插件B和插件C,而解析的時(shí)候是遍歷list,所以解析的時(shí)候也是按照:插件A,插件B和插件C的順序,但是執(zhí)行的時(shí)候就要反過(guò)來(lái)了,執(zhí)行的時(shí)候是按照:插件C,插件B和插件A的順序進(jìn)行執(zhí)行。
PageHelper插件的使用
上面我們了解了在MyBatis中的插件是如何定義以及MyBatis中是如何處理插件的,接下來(lái)我們就以經(jīng)典分頁(yè)插件PageHelper為例來(lái)進(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對(duì)象
????????SqlSessionFactory?sqlSessionFactory?=?new?SqlSessionFactoryBuilder().build(inputStream);
????????//創(chuàng)建SqlSession對(duì)象
????????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é)果:

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

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

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

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

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

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

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

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


