mybatis原始碼學習:外掛定義+執行流程責任鏈

天喬巴夏丶發表於2020-04-26


前文傳送門:
mybatis原始碼學習:從SqlSessionFactory到代理物件的生成
mybatis原始碼學習:一級快取和二級快取分析
mybatis原始碼學習:基於動態代理實現查詢全過程

一、自定義外掛流程

  • 自定義外掛,實現Interceptor介面。

  • 實現intercept、plugin和setProperties方法。

  • 使用@Intercepts註解完成外掛簽名。

  • 在主配置檔案註冊外掛。

/**
 * 自定義外掛
 * Intercepts:完成外掛簽名,告訴mybatis當前外掛攔截哪個物件的哪個方法
 *
 * @author Summerday
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = Statement.class)
})
public class MyPlugin implements Interceptor {
    /**
     * 攔截目標方法執行
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyPlugin.intercept getMethod: "+invocation.getMethod());
        System.out.println("MyPlugin.intercept getTarget:"+invocation.getTarget());
        System.out.println("MyPlugin.intercept getArgs:"+ Arrays.toString(invocation.getArgs()));
        System.out.println("MyPlugin.intercept getClass:"+invocation.getClass());
        //執行目標方法
        Object proceed = invocation.proceed();
        //返回執行後的返回值
        return proceed;
    }

    /**
     * 包裝目標物件,為目標物件建立一個代理物件
     *
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("MyPlugin.plugin :mybatis將要包裝的物件:"+target);
        //藉助Plugin類的wrap方法使用當前攔截器包裝目標物件
        Object wrap = Plugin.wrap(target, this);
        //返回為當前target建立的動態代理
        return wrap;
    }

    /**
     * 將外掛註冊時的properties屬性設定進來
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("外掛配置的資訊:" + properties);
    }
}

xml配置註冊外掛

    <!--註冊外掛-->
    <plugins>
        <plugin interceptor="com.smday.interceptor.MyPlugin">
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </plugin>
    </plugins>

二、測試外掛

在這裡插入圖片描述

三、原始碼分析

1、inteceptor在Configuration中的註冊

關於xml檔案的解析,當然還是需要從XMLConfigBuilder中查詢,我們很容易就可以發現關於外掛的解析:

  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //獲取到全類名
        String interceptor = child.getStringAttribute("interceptor");
        //獲取properties屬性
        Properties properties = child.getChildrenAsProperties();
        //通過反射建立例項
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        //設定屬性
        interceptorInstance.setProperties(properties);
        //在Configuration中新增外掛
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

  public void addInterceptor(Interceptor interceptor) {
    //interceptorChain是一個儲存interceptor的Arraylist
    interceptorChain.addInterceptor(interceptor);
  }

此時初始化成功,我們在配置檔案中定義的外掛,已經成功加入interceptorChain。

2、基於責任鏈的設計模式

我們看到chain這個詞應該並不會陌生,我們之前學習過的過濾器也存在類似的玩意,什麼意思呢?我們以Executor為例,當建立Executor物件的時候,並不是直接new Executor然後返回:

在這裡插入圖片描述

在返回之前,他進行了下面的操作:

executor = (Executor) interceptorChain.pluginAll(executor);

我們來看看這個方法具體幹了什麼:

  public Object pluginAll(Object target) {
    //遍歷所有的攔截器
    for (Interceptor interceptor : interceptors) {
        //呼叫plugin,返回target包裝後的物件
      target = interceptor.plugin(target);
    }
    return target;
  }

很明顯,現在它要從chain中一一取出interceptor,並依次呼叫各自的plugin方法,暫且不談plugin的方法,我們就能感受到責任鏈的功能:讓一個物件能夠被鏈上的任何一個角色寵幸,真好。

3、基於動態代理的plugin

那接下來,我們就成功進入我們自定義plugin的plugin方法:

在這裡插入圖片描述

  //看看wrap方法幹了點啥
  public static Object wrap(Object target, Interceptor interceptor) {
    //獲取獲取註解的資訊,攔截的物件,攔截的方法,攔截方法的引數。
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //獲取當前物件的Class
    Class<?> type = target.getClass();
    //確認該物件是否為我們需要攔截的物件
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //如果是,則建立其代理物件,不是則直接將物件返回
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

getSignatureMap(interceptor)方法:其實就是獲取註解的資訊,攔截的物件,攔截的方法,攔截方法的引數。

  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //定位到interceptor上的@Intercepts註解
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
	//如果註解不存在,則報錯
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    //獲取@Signature組成的陣列
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    
    for (Signature sig : sigs) {
      //先看map裡有沒有methods set
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        //沒有再建立一個
        methods = new HashSet<Method>();
        //class:methods設定進去
        signatureMap.put(sig.type(), methods);
      }
      try {
        //獲取攔截的方法
        Method method = sig.type().getMethod(sig.method(), sig.args());
        //加入到set中
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }

getAllInterfaces(type, signatureMap)方法:確定是否為攔截物件

  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      //介面型別
      for (Class<?> c : type.getInterfaces()) {
        //如果確實是攔截的物件,則加入interfaces set
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      //從父介面中檢視
      type = type.getSuperclass();
    }
    //最後set裡面存在的元素就是要攔截的物件
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

我們就可以猜測,外掛只會對我們要求的物件和方法進行攔截。

4、攔截方法的intercept(invocation)

確實,我們一路debug,遇到了Executor、ParameterHandler、ResultHandler都沒有進行攔截,然而,當StatementHandler物件出現的時候,就出現了微妙的變化,當我們呼叫代理的方法必然會執行其invoke方法,不妨來看看:

在這裡插入圖片描述

ok,此時進入了我們定義的intercept方法,感覺無比親切。

在這裡插入圖片描述

  //排程被代理物件的真實方法
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

如果有多個外掛,每經過一次wrap都會產生上衣個物件的代理物件,此處反射呼叫的方法也是上衣個代理物件的方法。接著,就還是執行目標的parameterize方法,但是當我們明白這些執行流程的時候,我們就可以知道如何進行一些小操作,來自定義方法的實現了。

四、外掛開發外掛pagehelper

外掛文件地址:https://github.com/pagehelper/Mybatis-PageHelper

這款外掛使分頁操作變得更加簡便,來一個簡單的測試如下:

1、引入相關依賴

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.2</version>
        </dependency>

2、全域性配置

    <!--註冊外掛-->
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
    </plugins>

3、測試分頁

    @Test
    public void testPlugin(){
        //查詢第一頁,每頁3條記錄
        PageHelper.startPage(1,3);
        List<User> all = userDao.findAll();
        for (User user : all) {
            System.out.println(user);
        }
    }

在這裡插入圖片描述

五、外掛總結

參考:《深入淺出MyBatis技術原理與實戰》

  • 外掛生成地是層層代理物件的責任鏈模式,其中設計反射技術實現動態代理,難免會對效能產生一些影響。
  • 外掛的定義需要明確需要攔截的物件、攔截的方法、攔截的方法引數。
  • 外掛將會改變MyBatis的底層設計,使用時務必謹慎。

相關文章