MyBatis插件實現(xiàn)原理

1. 插件能夠攔截的對象和方法

MyBatis 允許你在映射語句執(zhí)行過程中的某一點進行攔截調(diào)用。默認情況下婶溯,MyBatis 允許使用插件來攔截的方法調(diào)用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

2. 插件的工作流程

解析注冊---->代理----->執(zhí)行時攔截

  • 解析注冊是在解析mybtais-config.xml配置文件時艘包,解析plugin標簽時完成褒颈。把所有的插件都放到interceptorChain中進行保存廓推。這是一個ArrayList甥温。

  • 代理是在創(chuàng)建上面四個對象時執(zhí)行锻煌。具體的代理邏輯,下面分析姻蚓。

  • 執(zhí)行時攔截宋梧,攔截器會攔截配置的對象的方法。例如我有一個攔截器攔截Executor的query方法狰挡,當Executor執(zhí)行query方法時捂龄,會先走攔截器的邏輯释涛。思想上有些類似于Spring的AOP。

3. 一次自定義插件使用的分析

3.1 插件配置

        <plugins>
            <plugin interceptor="com.tomas.mysql.interceptor.MyPageInterceptor">
            </plugin>
        </plugins>

代碼實現(xiàn)

實現(xiàn)Interceptor接口倦沧,實現(xiàn)接口的方法唇撬,在intercept方法中實現(xiàn)自己的插件功能。

@Intercepts({@Signature(type = Executor.class,method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MyPageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("自定義插件開始執(zhí)行...");
        Executor executor = (Executor)invocation.getTarget();
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
        BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
        ResultHandler resultHandler = (ResultHandler) args[3];
        String countSql = boundSql.getSql();
        System.out.println("獲取到SQL語句:"+countSql);
        countSql = countSql + " limit 5";

        SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(),countSql,boundSql.getParameterMappings());
        Field field = MappedStatement.class.getDeclaredField("sqlSource");
        field.setAccessible(true);
        field.set(ms,sqlSource);
        
        try{
            return invocation.proceed();
        }finally {
            System.out.println("自定義插件執(zhí)行結(jié)束!!!");
        }
    }

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

    @Override
    public void setProperties(Properties properties) {
    }
}

3.2 插件的代理

上面的例子中展融,我們的插件要攔截Executor的query方法窖认。

Mybatis在運行的時候是怎么進行攔截的呢?

  • 首先告希,openSession()的時候會創(chuàng)建一個executor執(zhí)行器扑浸,最后會對這個執(zhí)行器進行插件植入操作。executor = (Executor) interceptorChain.pluginAll(executor);

  • 然后燕偶,把executor作為target進行代理喝噪,當配置多個攔截器的時候,會進行多層代理指么。代理流程如下:

InterceptorChain.java
// 對executor即進行代理
public Object pluginAll(Object target) {
  // 第一步 interceptor是配置的插件
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
MyPageInterceptor.java
// 第二步酝惧,到具體的實現(xiàn)類中調(diào)用plugin方法
@Override
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

不管有沒有配置攔截器,可以被攔截的四種對象在創(chuàng)建的過程中都會有攔截器的包裝操作涧尿,但是到底要不要進行包裝呢系奉,由if (interfaces.length > 0) {這句代碼進行控制檬贰。

Plugin.java
// 第三步姑廉,使用jdk的動態(tài)代理
public static Object wrap(Object target, Interceptor interceptor) {
  // 獲取攔截器要攔截的接口的簽名,只要攔截器配置就會有值
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  // 獲取目標類的類型
  Class<?> type = target.getClass();
  /** 
   * 判斷目標類是不是本次要被代理的接口翁涤。因為mybatis的攔截器可以攔截Executor桥言、ParameterHandler、
   * ResultSetHandler葵礼、StatementHandler四種對象号阿。這四個對象的創(chuàng)建過程中都會有攔截器邏輯的判斷,都          * 走這個代碼從interceptor中判斷出這個攔截器可以攔截哪些對象鸳粉,從target中知道這個對象是什么扔涧,從而可以     * 判斷本次要不要進行代理操作。
   **/
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

到此為止届谈,被攔截的對象的代理工作就完成了枯夜。

3.4 插件的執(zhí)行

3.4.1 JDK動態(tài)代理

JDK動態(tài)代理后方法的執(zhí)行順序:

// jdk動態(tài)代理的一個例子,重寫的invoke()方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    before();
    Object res = method.invoke(person,args);
    return res;
}
  1. 代理對象調(diào)用方法艰山。
  2. 執(zhí)行h的invoke()方法湖雹。
  3. 進行前置增強處理。
  4. method.invoke真正執(zhí)行要到用的方法曙搬。
3.4.2 Mybatis中動態(tài)代理的調(diào)用

我們已經(jīng)知道摔吏,Mybatis是使用JDK的動態(tài)代理鸽嫂,去動態(tài)的代理真正要執(zhí)行的對象的方法。從而實現(xiàn)增強的功能征讲。因此据某,當要執(zhí)行的對象進行方法調(diào)用的時候,會先執(zhí)行實現(xiàn)了InvocationHandler類的invoke方法诗箍。即:

Plugin類中的invoke方法哗脖。

// plugin是觸發(fā)管理類,當被代理的對象的方法執(zhí)行的時候扳还,先執(zhí)行這個invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
        // 自定義的攔截方法的實現(xiàn)
      return interceptor.intercept(new Invocation(target, method, args));
    }
    // 不許要被攔截的方法才避,直接調(diào)用
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }

此時,當執(zhí)行executor的query方法時氨距,就會調(diào)用到MyPageInterceptor類中的intercept方法桑逝。

            try{
            return invocation.proceed(); // 這句話需要關(guān)注下
        }finally {
            System.out.println("自定義插件執(zhí)行結(jié)束!!!");
        }

當我們執(zhí)行完自己實現(xiàn)的插件的邏輯之后,還需要執(zhí)行真正要執(zhí)行的方法俏让。什么時候去執(zhí)行呢楞遏?

我們知道jdk動態(tài)代理中是使用method.invoke(target, args);去執(zhí)行真正的方法的調(diào)用。而invocation.proceed();做的就是這個事情首昔。

Invocation.java
public Object proceed() throws InvocationTargetException, IllegalAccessException {
  return method.invoke(target, args);
}

到這里寡喝,攔截器就執(zhí)行完了。

4. 關(guān)鍵的類

InterceptorChain: 用于保存攔截的對象勒奇,使用責任鏈模式的思想预鬓,把所有的攔截器都添加到這個鏈中。

Interceptor接口: Mybtais框架給提供的接口赊颠,所有自己要實現(xiàn)的攔截器都要實現(xiàn)這個接口格二。相當于給定義了一個規(guī)范。

Plugin類:Mybtais框架給提供的類竣蹦,實現(xiàn)了InvocationHandler接口顶猜,在進行invoke方法調(diào)用的時候,它的對象可以作為h參數(shù)痘括。

Invocation類:Mybtais框架給提供的一個封裝的類长窄。把invoke方法的三個參數(shù)封裝了起來,應(yīng)該是為了更加方便的調(diào)用吧纲菌。

5. 總結(jié)一下調(diào)用流程

  1. executor執(zhí)行query方法挠日。
  2. 發(fā)現(xiàn)自己被代理了,然后去執(zhí)行invoke方法驰后。
  3. 此時就來到了Pluigin的invoke方法中肆资,繼續(xù)去執(zhí)行interceptor的intercept()方法。
  4. 自定義邏輯執(zhí)行完之后執(zhí)行一下invocation.proceed(),就完成了method.invoke()的調(diào)用灶芝。

最后借用一下青山老師的圖:


調(diào)用過程.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末郑原,一起剝皮案震驚了整個濱河市唉韭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌犯犁,老刑警劉巖属愤,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異酸役,居然都是意外死亡住诸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門涣澡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贱呐,“玉大人,你說我怎么就攤上這事入桂⊙俎保” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵抗愁,是天一觀的道長馁蒂。 經(jīng)常有香客問我,道長蜘腌,這世上最難降的妖魔是什么沫屡? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮撮珠,結(jié)果婚禮上沮脖,老公的妹妹穿的比我還像新娘。我一直安慰自己劫瞳,他們只是感情好倘潜,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布绷柒。 她就那樣靜靜地躺著志于,像睡著了一般。 火紅的嫁衣襯著肌膚如雪废睦。 梳的紋絲不亂的頭發(fā)上伺绽,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音嗜湃,去河邊找鬼奈应。 笑死,一個胖子當著我的面吹牛购披,可吹牛的內(nèi)容都是我干的杖挣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼刚陡,長吁一口氣:“原來是場噩夢啊……” “哼惩妇!你這毒婦竟也來了株汉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤歌殃,失蹤者是張志新(化名)和其女友劉穎乔妈,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氓皱,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡路召,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了波材。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片股淡。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖廷区,靈堂內(nèi)的尸體忽然破棺而出揣非,到底是詐尸還是另有隱情,我是刑警寧澤躲因,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布早敬,位于F島的核電站,受9級特大地震影響大脉,放射性物質(zhì)發(fā)生泄漏搞监。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一镰矿、第九天 我趴在偏房一處隱蔽的房頂上張望琐驴。 院中可真熱鬧,春花似錦秤标、人聲如沸绝淡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牢酵。三九已至,卻和暖如春衙猪,著一層夾襖步出監(jiān)牢的瞬間馍乙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工垫释, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丝格,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓棵譬,卻偏偏與公主長得像显蝌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子订咸,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345