MyBatis源碼系列--5.MyBatis 插件原理與自定義插件

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方法,驗證了第四個問題是正確的

插件調(diào)用時序圖.jpg

總結(jié)一下4個對象


image.png

——學自咕泡學院

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末稽揭,一起剝皮案震驚了整個濱河市俺附,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌溪掀,老刑警劉巖事镣,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡璃哟,警方通過查閱死者的電腦和手機氛琢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來随闪,“玉大人阳似,你說我怎么就攤上這事☆戆椋” “怎么了撮奏?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長当宴。 經(jīng)常有香客問我畜吊,道長,這世上最難降的妖魔是什么户矢? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任玲献,我火速辦了婚禮,結(jié)果婚禮上梯浪,老公的妹妹穿的比我還像新娘捌年。我一直安慰自己,他們只是感情好挂洛,可當我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布礼预。 她就那樣靜靜地躺著,像睡著了一般抹锄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荠藤,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天伙单,我揣著相機與錄音,去河邊找鬼哈肖。 笑死吻育,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的淤井。 我是一名探鬼主播布疼,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼币狠!你這毒婦竟也來了游两?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤漩绵,失蹤者是張志新(化名)和其女友劉穎贱案,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體止吐,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡宝踪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年侨糟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘩燥。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡秕重,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出厉膀,到底是詐尸還是另有隱情溶耘,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布站蝠,位于F島的核電站汰具,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏菱魔。R本人自食惡果不足惜留荔,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望澜倦。 院中可真熱鬧聚蝶,春花似錦、人聲如沸藻治。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桩卵。三九已至验靡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間雏节,已是汗流浹背胜嗓。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钩乍,地道東北人辞州。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像寥粹,于是被迫代替她去往敵國和親变过。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,974評論 2 355