1. 程式人生 > >MyBatis框架原理4:外掛

MyBatis框架原理4:外掛

外掛的定義和作用

首先引用MyBatis文件對外掛(plugins)的定義:

MyBatis 允許你在已對映語句執行過程中的某一點進行攔截呼叫。預設情況下,MyBatis 允許使用外掛來攔截的方法呼叫包括:

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

這些類中方法的細節可以通過檢視每個方法的簽名來發現,或者直接檢視 MyBatis 發行包中的原始碼。 如果你想做的不僅僅是監控方法的呼叫,那麼你最好相當瞭解要重寫的方法的行為。 因為如果在試圖修改或重寫已有方法的行為的時候,你很可能在破壞 MyBatis 的核心模組。 這些都是更低層的類和方法,所以使用外掛的時候要特別當心。

Mybatis外掛所攔截的4個物件正是在之前的文章MyBatis框架原理2:SqlSession執行過程中介紹的4個實現核心功能的介面。那麼外掛攔截這4個介面能做什麼呢?根據之前文章對4個介面的介紹,可以猜測到:

  • Executor是SqlSession整個執行過程的總指揮,同時還對快取進行操作,通過外掛可以使用自定義的快取,比如mybatis-enhanced-cache外掛。
  • StatementHandler負責SQL的編譯和執行,通過外掛可以改寫SQL語句。
  • ParameterHandler負責SQL的引數設定,通過外掛可以改變引數設定。
  • ResultSetHandler負責結果集對映和儲存過程輸出引數的組裝,通過外掛可以對結果集對映規則改寫。

外掛的原理

在理解外掛原理之前,得先搞清楚以下三個概念:

  • 動態代理 代理模式是一種給真實物件提供一個代理物件,並由代理物件控制對真實物件的引用的一種設計模式,動態代理是在程式執行時動態生成代理類的模式,JDK動態代理物件是由java提供的一個Proxy類和InvocationHandler介面以及一個真實物件的介面生成的。通常InvocationHandler的實現類持有一個真實物件欄位和定義一個invoke方法,通過Proxy類的newProxyInstance方法就可以生成這個真實物件的代理物件,通過代理物件排程方法實際就是呼叫InvocationHandler實現類的invoke方法,在invoke方法中可以通過反射實現呼叫真實物件的方法。

  • 攔截器(Interceptor) 動態代理物件可以對真實物件方法引用,是因為InvocationHandler實現類持有了一個真實物件的欄位,通過反射就可以實現這個功能。如果InvocationHandler實現類再持有一個Interceptor介面的實現類,Interceptor介面定義了一個入參為真實物件的intercept方法,Interceptor介面的實現類通過重寫intercept方法可以對真實物件的方法引用或者實現增強功能等等,也就是當我們再次使用這個動態代理物件排程方法時,可以根據需求對真實物件的方法做出改變。

    從這個Interceptor介面實現類的功能上來看,可以叫做真實物件方法的攔截器。於是我們再想一下,如果前面講到MyBatis的4個核心功能介面的實現類(比如PreparedStatementHandler)是一個真實物件,我們通過JDK動態代理技術生成一個代理物件,並且生成代理類所需的InvocationHandler實現類同時還持有了一個Interceptor介面實現類,通過使用代理物件排程方法,我們就可以根據需求對PreparedStatementHandler的功能進行增強。

    實際上MyBatis確實提供了這樣一個Interceptor介面和intercept方法,也提供了這樣的一個InvocationHandler介面的實現類,類名叫Plugin,它們都位於MyBatis的org.apache.ibatis.plugin包下。等等,那麼MyBatis的外掛不就是攔截器嗎?攔截器的原理都講完了,等下還怎麼講什麼外掛原理?

  • 責任鏈模式 我們通過JDK動態代理技術生成一個代理物件,代理的真實物件是個StatementHandler,並且持有StatementHandler的攔截器(外掛)。如果我們把這個代理物件視為一個target物件,再利用動態代理生成一個代理類,並且持有對這個target物件的攔截器(外掛),如果再把新生成的代理視為一個新的target類,同樣持有對新target類的攔截器(外掛),那麼我們就得到了一個像是被包裹了三層攔截器(外掛)的StatementHandler的代理物件:

當MyBatis每一次SqlSession會話需要引用到StatementHandler的方法時,如過符合上圖中攔截器3的攔截邏輯,則按攔截器3的定義的方法執行;如果不符合攔截邏輯,則將執行責任交給攔截器2處理,以此類推,這樣的模式叫做責任鏈模式。MyBatis全域性配置檔案裡可以配置多個外掛,多個外掛的執行就是按照這樣的責任鏈模式執行的。

通過對以上三點的理解,我們已經對MyBatis外掛原理已經有了初步認識,下面就通過原始碼看看MyBatis外掛是如何執行起來的。

外掛的執行過程

  • 外掛的介面 MyBatis提供了一個Interceptor介面,外掛必須實現這個介面,介面定義了3個方法如下:

    public interface Interceptor {
      // 執行外掛實現的方法,Invocation物件持有真實物件,可通過反射呼叫真實物件的方法
      Object intercept(Invocation invocation) throws Throwable;
      // 設定外掛攔截的物件target,通常呼叫Pulgin類的wrap方法生成一個代理類
      Object plugin(Object target);
      // 根據配置檔案初始化外掛
      void setProperties(Properties properties);
    
    }
    
  • 外掛的初始化 在MyBatis初始化時XMLConfigBuilderder的pluginElement方法對外掛配置檔案解析:

    private void pluginElement(XNode parent) throws Exception {
      if (parent != null) {
        for (XNode child : parent.getChildren()) {
          String interceptor = child.getStringAttribute("interceptor");
          Properties properties = child.getChildrenAsProperties();
          // 通過反射生成外掛的例項
          Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
          // 呼叫外掛配置引數
          interceptorInstance.setProperties(properties);
          // 將外掛例項儲存到Configuration物件中
          configuration.addInterceptor(interceptorInstance);
        }
      }
    }

    Configuration物件最終將解析出的外掛配置儲存在持有InterceptorChain物件中,InterceptorChain物件又是通過一個ArrayList來儲存所有外掛,可見在MyBatis初始化的時候外掛配置就已經載入好了,執行時就會根據外掛編寫的規則執行攔截邏輯。

    public class InterceptorChain {
        // 通過集合來儲存外掛
       private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
        // 通過責任鏈模式呼叫外掛plugin方法生成代理物件
       public Object pluginAll(Object target) {
            for (Interceptor interceptor : interceptors) {
                target = interceptor.plugin(target);
              }
             return target;
       }
      //  Configuration物件呼叫的新增外掛的方法
      public void addInterceptor(Interceptor interceptor) {
          interceptors.add(interceptor);
      }
    
      public List<Interceptor> getInterceptors() {
          return Collections.unmodifiableList(interceptors);
      }
    }
  • 外掛的執行 如果我們需要攔截MyBatis的Executor介面,Configuration在初始化Executor時就會通過責任鏈模式將初始化的Executor作為真實物件,呼叫InterceptorChain的pluginAll放法生成代理物件:

    
    
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 根據配置檔案生成相應Executor
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 呼叫InterceptorChain的pluginAll放法生成代理物件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
    } 
    

    InterceptorChain的 pluginAll方法呼叫外掛的plugin方法,plugin方法可以呼叫MyBatis提供的工具類Plugin類來生成代理物件,Plugin類實現了InvocationHandler,在Plugin類中定義invoke方法來實現攔截邏輯和執行外掛方法:

    public class Plugin implements InvocationHandler {
    
    // target為需要攔截的真實物件 
    private final Object target;
    // interceptor為外掛
    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);
      // 動態代理生成代理物件並返回
      if (interfaces.length > 0) {
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
      }
      return target;
    }
    
    @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)) {
          return interceptor.intercept(new Invocation(target, method, args));
        }
        // 直接呼叫真是物件的方法
        return method.invoke(target, args);
      } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
      }
    }
    ...

    外掛的intercept方法引數為Invocation物件,Invocation物件持有真實物件和一個proceed方法,proceed方法通過反射呼叫真實物件的方法。於是多個外掛生成的責任鏈模式的代理物件,就可以通過一層一層執行proceed方法來呼叫真實物件的方法。

外掛的開發

  • 自己編寫外掛必須繼承MyBatis的Interceptor介面

        public interface Interceptor {
        // 執行外掛實現的方法,Invocation物件持有真實物件,可通過反射呼叫真實物件的方法
        Object intercept(Invocation invocation) throws Throwable;
        // 設定外掛攔截的物件target,通常呼叫Pulgin類的wrap方法生成一個代理類
        Object plugin(Object target);
        // 根據配置檔案初始化外掛
        void setProperties(Properties properties);
    
      }
  • 使用@Intercepts和@Signature註解

      @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class ,Integet.class})})
      public class MyPlugin implements Interceptor {...}
    

    用@Intercepts註解申明是一個外掛,@Signature註解申明攔截的物件,方法和引數。上面的寫法表明了攔截了StatementHandler物件的prepare方法,引數是一個Connection物件和一個Integet。

  • 編寫攔截方法 MyBatis提供了一個Invocation工具類,通常我們將需要攔截的真實物件,方法及引數封裝在裡面作為一個引數傳給外掛的intercept方法,在外掛intercept方法裡可以編寫攔截邏輯和執行攔截方法,方法引數invocation可以通過反射呼叫被代理物件的方法:

        @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class ,Integet.class})})
      public class MyPlugin implements Interceptor {
           @override
           public Object intercept(Invocation invocation) throws Throwable {
           // do something ...
           // 呼叫被代理物件的方法
           invocation.proceed();
           // do something ...
           @override
           呼叫Plugin工具類生成代理物件
           public Object plugin(Object target){
               return Plugin.wrap(target, this);
           }
           ...
    
      }
    
  • 生成代理物件 MyBatis還提供了一個Plugin工具類,其中wrap方法用於生成代理類,invoke方法驗證攔截型別和方法,並選擇是否按攔截器的方法,程式碼如下:

        public class Plugin implements InvocationHandler {
    
    private final Object target; // 真實物件
    private final Interceptor interceptor; // 攔截器(外掛)
    private final Map<Class<?>, Set<Method>> signatureMap; // Map儲存簽名的型別,方法和引數資訊
    
    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) {
      // getSignatureMap方法通過反射獲取外掛裡@Intercepts和@Signature註解宣告的攔截型別,方法和引數資訊
      Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
      Class<?> type = target.getClass();
      從signatureMap中獲取攔截物件的型別
      Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
      // 生成代理物件,如果target的型別不是外掛裡註解宣告的型別則直接返回target不作攔截。
      if (interfaces.length > 0) {
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
      }
      return target;
    }
    
    @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)) {
          // 如果是宣告攔截的方法,則呼叫外掛的intercept方法執行攔截處理
          return interceptor.intercept(new Invocation(target, method, args));
        }
        // 如果不是宣告攔截的方法,則直接呼叫真實物件的方法
        return method.invoke(target, args);
      } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
      }
    }
    ...
    

總結

MyBatis外掛執行依靠Java動態代理技術實現,雖然原理很簡單,但是編寫外掛涉及到修改MyBatis框架底層的介面,需要十分謹慎,做為初學者,最好使用現成的外掛。