前言
提到插件,相信大家都知道,插件的存在主要是用來改變或者增強原有的功能,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})})
public class MyPlugin implements Interceptor {
/**
* 這個方法會直接覆蓋原有方法
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("成功攔截了Executor的query方法,在這里我可以做點什么");
return invocation.proceed();//調用原方法
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);//把被攔截對象生成一個代理對象
}
@Override
public void setProperties(Properties properties) {//可以自定義一些屬性
System.out.println("自定義屬性:userName->" + properties.getProperty("userName"));
}
}
@Intercepts是聲明當前類是一個攔截器,后面的@Signature是標識需要攔截的方法簽名,通過以下三個參數來確定
- type:被攔截的類名。
- method:被攔截的方法名
- args:標注方法的參數類型
2、我們還需要在mybatis-config中配置好插件。
<plugins>
<plugin interceptor="com.lonelyWolf.mybatis.plugin.MyPlugin">
<property name="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方法:
其他兩個對象就不在舉例了,其實搜一下全局就很明顯了:
PS:
四個對象初始化的時候都會調用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;
public class MyBatisByPageHelp {
public static void main(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