1. 程式人生 > >Mybatis外掛實戰進階篇

Mybatis外掛實戰進階篇

在上一篇文章中我們已經全面的瞭解了Mybatis外掛相關實現原理,以及它在實際開發應用中的作用,用的好的話,可以實現系統鬆耦合,動態擴充套件。這一篇廢話不多說,我將帶你領略外掛在處理含巢狀方法場景的介面時實戰編寫指南,以及那些我踩過的坑。

要代理的介面
這裡講的是一種特殊場景,即要代理的介面有A、B兩個方法,在實際業務流程上A方法會呼叫B方法。

要實現的特性
希望運用Mybatis外掛機制來在A方法before切入一段預處理過程,after切入一段後置處理,B方法也是如此,如此一來為如下期望順序:

beforeA
A
beforeB
B
afterB
afterA

太簡了你就入坑了


小強同學拿到這份需求,立馬說太簡了,他於是寫了兩個InterceptorB,InterceptorA。
然後先用InterceptorB去攔截B方法產生代理targetB,然後兩用 InterceptorA去plugin
targetB產生targetA,
最終呼叫targetA.A()
結果輸出如下:

beforeA
A
B
afterA

此時你或許已傻眼

帶你出坑
上述問題出在targetA呼叫A方法時,呼叫的確實是代理物件的方法,但是在準備呼叫B方法時,此時棧幀進入的是原物件的A方法中,因為控制呼叫B方法的時機是由原物件的方法中去控制的,原物件的A方法呼叫的B方法必然不是代理物件的B方法,所以會出現以上問題,這也是因為Mybatis是基於JDK動態介面代理的原因,其本質是代理物件,這並不像JAVA本身的類方法重寫機制,可以根據物件例項的不同,方法執行能夠動態繫結。

制定策略
綜上我們發現問題根本原因出在:如果被呼叫方法只要巢狀在本類方法中,我們是永遠不可能達到以上的要求,因為我們在A方法進去的時候,永遠調不到代理的B方法,所以我們要改變這個關鍵的現狀。
具體策略如下:
首先還是InterceptorB攔截B方法,然後再是InterceptorA去重寫*A方法,注意這裡我用到的是重寫*兩字,重寫的時候要注意A方法的原來邏輯大體不變,而在呼叫B方法的時候,我們只需要通用invocation物件拿到上面InterceptorB代理出來的targetB物件,這個時候cast為我們代理的介面型別,這個時候再呼叫B方法,此時呼叫的B方法才是我們已經攔截好的B方法,最後在重寫的這個程式碼塊之前之後加上我們對A攔截的相關邏輯,最終就實現了以上需求。

實戰程式碼演示
筆者以上所講都是用比較通俗易懂的話娓娓道來,也許不夠詞彙不夠專業,但是我相信你如果認真看,一定能明白其中的訣竅。而以上的剖析也絕對是實戰之下的感受。在此貼出我已經碼出的Demo(比較簡單,我就不打擾大家看了,敬請品賞):
People

package org.apache.ibatis.plugin.test.nest;

public interface People {

    void work();

    void programme();
}

Programmer

package org.apache.ibatis.plugin.test.nest;

public class Programmer implements People {
    @Override
    public void work() {

        System.out.println("8點40準時開站會!");

        programme();


    }

    @Override
    public void programme() {
        System.out.println("程式設計中");
    }
}

ProgrammeInterceptor

package org.apache.ibatis.plugin.test.nest;

import org.apache.ibatis.plugin.*;

import java.util.Properties;

@Intercepts({
        @Signature(type = People.class, method = "programme", args = {})
})
public class ProgrammeInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("程式設計之前我得好好理清需求");
        invocation.proceed();
        System.out.println("程式設計之後我得執行單元測試並部署到測試環境進行驗證");
        return null;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

WorkInterceptor

package org.apache.ibatis.plugin.test.nest;

import org.apache.ibatis.plugin.*;

import java.util.Properties;

@Intercepts({
        @Signature(type = People.class, method = "work", args = {})
})
public class WorkInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        System.out.println("坐巴士去上班");

        System.out.println("8點40準時開站會!");
        People programmer = (People) invocation.getTarget();
        programmer.programme();

        System.out.println("坐巴士回家");

        return null;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

NestTest

package org.apache.ibatis.plugin.test.nest;

import org.apache.ibatis.plugin.InterceptorChain;

public class NestTest {

    public static void main(String[] args) {
        InterceptorChain interceptorChain = new InterceptorChain();
        interceptorChain.addInterceptor(new ProgrammeInterceptor());
        interceptorChain.addInterceptor(new WorkInterceptor());

        People target = new Programmer();
        People wrappedProgrammer = (People) interceptorChain.pluginAll(target);
        wrappedProgrammer.work();
    }

}

最後注意點
筆者在實戰時,其實並不是最終測試直接是期望效果,最終剖析發現,我在攔截器的註解@Signature上申明type時,type並不是介面,導致了最終執行有問題,這就要提到JDK動態代理,實質是隻支援介面的動態代理,並不支援非介面類,而代理出來的物件的實現類是在執行時由底層技術實現的,它在並非是(如上)Programmer的子類,而是People的子類,也就是說People是介面,它是代理實現類與Programmer類的父類,而代理實現類與Programmer類並沒有直接交集,它們只是在切入代理的實現上有互動而已,同時我們發現我們要代理Programmer類的方法A和B,那麼這些要代理的方法A和B也必須抽取到上一層的介面People中,而用攔截器去真正代理People介面,實際執行傳入Programmer的例項即可。

也許以上很多可能表述有誤,歡迎指正,最後謝謝你的閱讀,你的支援是我寫作的動力。