MyBatis 通過提供插件機制,讓我們可以根據(jù)自己的需要去增強 MyBatis 的功能
需要注意的是袖订,如果沒有完全理解 MyBatis 的運行原理和插件的工作方式氮帐,最好不要使用插件,因為它會改變系底層的工作邏輯洛姑,給系統(tǒng)帶來很大的影響上沐。
MyBatis 的插件可以在不修改原來的代碼的情況下,通過攔截的方式楞艾,改變四大核心對象的行為(在上一篇已經(jīng)知曉)参咙,比如處理參數(shù)龄广,處理 SQL,處理結(jié)果
它內(nèi)部用到兩個設(shè)計模式
- 代理模式
比如它可以在不修改對象的代碼的情況下蕴侧,對對象的行為進行修改择同,比如說在原來的方法前面做
一點事情,在原來的方法后面做一點事情 - 責任鏈模式
我們可以定義很多的插件净宵,那么這種所有的插件會形成一個鏈路敲才,然后層層攔截去處理所有插件
參考官網(wǎng):http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins
插件編寫與注冊
(基于 spring-mybatis)運行自定義的插件,需要 3 步择葡,我們以 PageHelper 為例:
- 第一步紧武,編寫自己的插件類
1.實現(xiàn) Interceptor 接口,這個是所有的插件必須實現(xiàn)的接口敏储。
2.添加@Intercepts({@Signature()})脏里,指定攔截的對象和方法、方法參數(shù) 方法名稱+參數(shù)類型虹曙,構(gòu)成了方法的簽名,決定了能夠攔截到哪個方法番舆。
3.實現(xiàn)接口的 3 個方法
// 用于覆蓋被攔截對象的原有方法(在調(diào)用代理對象 Plugin 的 invoke()方法時被調(diào)用)
Object intercept(Invocation invocation) throws Throwable;
// target 是被攔截對象酝碳,這個方法的作用是給被攔截對象生成一個代理對象,并返回它
Object plugin(Object target);
// 設(shè)置參數(shù)
void setProperties(Properties properties);
- 第二步恨狈,插件注冊疏哗,在 mybatis-config.xml 中注冊插件
<!--分頁插件的注冊-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 4.0.0以后版本可以不設(shè)置該參數(shù) ,可以自動識別
<property name="dialect" value="mysql"/> -->
<!-- 該參數(shù)默認為false -->
<!-- 設(shè)置為true時,會將RowBounds第一個參數(shù)offset當成pageNum頁碼使用 -->
<!-- 和startPage中的pageNum效果一樣-->
<property name="offsetAsPageNum" value="true"/>
<!-- 該參數(shù)默認為false -->
<!-- 設(shè)置為true時禾怠,使用RowBounds分頁會進行count查詢 -->
<property name="rowBoundsWithCount" value="true"/>
<!-- 設(shè)置為true時返奉,如果pageSize=0或者RowBounds.limit = 0就會查詢出全部的結(jié)果 -->
<!-- (相當于沒有執(zhí)行分頁查詢,但是返回結(jié)果仍然是Page類型)-->
<property name="pageSizeZero" value="true"/>
<!-- 3.3.0版本可用 - 分頁參數(shù)合理化吗氏,默認false禁用 -->
<!-- 啟用合理化時芽偏,如果pageNum<1會查詢第一頁,如果pageNum>pages會查詢最后一頁 -->
<!-- 禁用合理化時弦讽,如果pageNum<1或pageNum>pages會返回空數(shù)據(jù) -->
<property name="reasonable" value="true"/>
<!-- 3.5.0版本可用 - 為了支持startPage(Object params)方法 -->
<!-- 增加了一個`params`參數(shù)來配置參數(shù)映射污尉,用于從Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默認值 -->
<!-- 不理解該含義的前提下,不要隨便復制該配置 -->
<property name="params" value="pageNum=start;pageSize=limit;"/>
<!-- 支持通過Mapper接口參數(shù)來傳遞分頁參數(shù) -->
<property name="supportMethodsArguments" value="true"/>
<!-- always總是返回PageInfo類型,check檢查返回類型是否為PageInfo,none返回Page -->
<property name="returnPageInfo" value="check"/>
</plugin>
</plugins>
- 第三步往产,插件登記
MyBatis 啟 動 時 掃 描 <plugins> 標 簽 被碗, 注 冊 到 Configuration 對 象 的InterceptorChain 中,property 里面的參數(shù)仿村,會調(diào)用 setProperties()方法處理锐朴。
代理和攔截是怎么實現(xiàn)的
我們先來看看4個問題
1.四大對象什么時候被代理?代理對象是什么時候創(chuàng)建的蔼囊?
Executor 是 openSession() 的 時 候 創(chuàng) 建 的 焚志;
StatementHandler 是SimpleExecutor.doQuery()創(chuàng)建的衣迷;
里面包含了處理參數(shù)的 ParameterHandler 和處理 結(jié)果集的 ResultSetHandler 的創(chuàng)建,
創(chuàng)建之后即調(diào)用 InterceptorChain.pluginAll()娩嚼,返回層層代理后的對象蘑险。-
2.多個插件的情況下,代理對象能不能再被其他對象所代理岳悟?代理順序和調(diào)用順序的關(guān)系佃迄?
可以被代理,順序如下圖:
image.png 3.誰來創(chuàng)建代理對象贵少?
Plugin類呵俏,在重寫的plugin() 方法里面可以直接調(diào)用return Plugin.wrap(target, this);返回代理對象4.被代理后,調(diào)用的是什么方法滔灶?怎么調(diào)用到原被代理對象的方法普碎?
因為代理類是 Plugin,所以最后調(diào)用的是 Plugin 的 invoke()方法录平。它先調(diào)用了定義的攔截器的 intercept()方法麻车。可以通過 invocation.proceed()調(diào)用到被代理對象被攔截的方法
帶這4個問題和答案斗这,以PageInterceptor為例动猬,跟進代碼來核實一下
首先mybatis-config.xml配置插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
...
打開PageInterceptor 的源碼
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class PageInterceptor implements Interceptor {
...
// 用于覆蓋被攔截對象的原有方法(在調(diào)用代理對象 Plugin 的 invoke()方法時被調(diào)用)
public Object intercept(Invocation invocation) throws Throwable {
...
//這個內(nèi)部邏輯大概就是重新組織sql,把sql加上分頁的語句表箭,
//根據(jù)方言不同赁咙,生成不同數(shù)據(jù)庫的分頁sql,
//這個分頁的值(比如pageNum,pageSize),
//是利用PageHelper.startPage(1, 10); 來設(shè)置的
//內(nèi)部使用了ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
//因為使用了ThreadLocal,所以直接從這里可以獲取到分頁值免钻,重組sql語句即可
}
//target 是被攔截對象彼水,這個方法的作用是給被攔截對象生成一個代理對象,并返回它
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//這個方法就是獲取配置文件中的配置項,并設(shè)置參數(shù)
public void setProperties(Properties properties) {
...
this.msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
String dialectClass = properties.getProperty("dialect");
...
}
我們先來看Plugin.wrap(target, this); 方法
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
可知极舔,最終返回一個jdk的動態(tài)代理勃刨,代理對象就是Plugin妥色,驗證了第三個問題是正確的
進入Plugin類
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
//最關(guān)鍵的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
看到invoke方法茄螃,可以知道宫屠,最終還是會調(diào)用具體插件的intercept方法,驗證了第四個問題是正確的
總結(jié)一下4個對象
——學自咕泡學院