前言
提到插件,相信大家都知道,插件的存在主要是用來改變或者增強原有的功能,MyBatis中也一樣。
然而如果我們對MyBatis的工作原理不是很清楚的話,最好不要輕易使用插件,否則的話如果因為使用插件導緻了底層工作邏輯被改變,很可能會出現很多意料之外的問題。。
整理了100+個Java項目視訊+源碼+筆記本文主要會介紹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是辨別需要攔截的方法簽名,通過以下三個參數來确定
(1)type:被攔截的類名
(2)method:被攔截的方法名
(3)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方法:
其他兩個對象就不在舉例了,其實搜一下全局就很明顯了:
四個對象初始化的時候都會調用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分頁插件了。等裝置。