深入理解Mybatis插件

Mybatis插件實現原理

本文如有任何紕漏、錯誤,請不吝指出,謝謝!

首先,我並沒有使用過 Mybatis的插件,但是這個和我寫這篇文章並不衝突,估計能真正使用到插件的人也比較少,寫這篇文章的目的主要是看源碼時稍微看到了下它的插件實現,發現還挺繞的,於是就特意琢磨了下,然後留了一個記錄。

mybatis中的插件,也就是攔截器interceptor,也挺有意思的。

它的簡單使用,就直接拿文檔中的示例來簡單說下

使用

使用方式很簡單

// 使用這個註解,表明這是一個攔截器  
@Intercepts(
  // 方法簽名
  {@Signature(
     // 被攔截方法所在的類
     type= Executor.class,
     // 被攔截方法名
     method = "update",
     // 被攔截方法參數列表類型
     args = {MappedStatement.class ,Object.class})}
)
 public class ExamplePlugin implements Interceptor {
     // 攔截時具體行爲
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
       // implement pre-processing if needed
       Object returnObject = invocation.proceed();
       // implement post-processing if needed
       return returnObject;
     }
 }

然後如果是xml配置的話

<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

如果是 SpringBoot的話,應該是配合自動配置使用,將上面的類使用@Component註解,交由Spring容器管理,然後註冊到mybatisInterceptorChain

mybaits目前支持攔截的類和方法,有下面這些

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

實現原理

有可能從上面的使用猜出來它的攔截器的實現原理,那就是動態代理的方式,只不過 mybatis這裏並不是很直接的來使用代理,繞了個彎,於是給人感覺特別暈,也說不好這個實現是不是有些問題

就先從Executor說起吧

我們從SqlSessionFactory獲取一個SqlSession時,會創建一個新的Executor實例,這個實際的創建動作在這裏

org.apache.ibatis.session.Configuration#newExecutor

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  // 根據設置的執行器類型,創建相應類型的執行器
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  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);
  }
  // 將原始執行器對象,包裝下,生成一個新的執行器,代理後的對象
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

InterceptorChain是一個管家,有點類似於FilterChain,但是注意,其實差別非常的大

public class InterceptorChain {
	// 所有攔截器列表
  private final List<Interceptor> interceptors = new ArrayList<>();

  // 這裏可能會是層層代理對象,一套又一套的,具體取決於攔截器的個數和被攔截的
  // 方法所在的類
  // 配置的攔截器數和每一次代理對象生成次數並不相同,會小於等於攔截器的個數
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    // 返回最後一次創建的代理對象
    return target;
  }
  // 註冊攔截器的
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
}

Interceptor是一個接口,也就是mybatis直接暴露給用戶使用的需要用戶實現的攔截器接口

public interface Interceptor {
   // 實現類填充自己的邏輯,參數爲Invocation,
  Object intercept(Invocation invocation) throws Throwable;

   // 默認方法,創建代理對象
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
   // 實現類去做些事情
  default void setProperties(Properties properties) {
    // NOP
  }

}

着重看下一這個

// 首先這是一個Jdk動態代理的InvocationHandler實現類
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
    Class<?> type = target.getClass();
    // 把被代理對象所有能在簽名Map中找到的直接實現的接口和祖先接口,查找出來
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 找到了就創建代理對象
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          // 創建一個InvocationHandler實例
          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)) {
        // 如果當前被調用的方法是需要被攔截的,那麼就執行我們自定義的攔截邏輯
        // interceptor是我們自定義的攔截器,在我們自定義的攔截器裏,需要獲取到
        // 原委託對象,被調用的方法,以及參數,這裏做了很好的封裝,將用戶的使用和
        // 具體的實現,做了一個完全的分離,用戶感知不到任何具體的實現
        // Invocation#proceed 就做了一件事 method.invoke(target,args);
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // 如果當前被調用的方法沒有被攔截,那麼直接調用原方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

  // 獲取攔截器指定的被攔截方法的方法簽名
  // key是被攔截方法的返回值類型
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    // 獲取註解 @Intercepts信息
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251 避免出現沒有具體的攔截信息的情況
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    // 獲取配置的被攔截方法的簽名信息
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    // 統統放到Map
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      try {
        // 將定義構建成實際的Method對象
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }
  // 獲取被代理對象所有的接口信息
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          // 如果被代理類接口和返回值類型一致,接口加進來
          interfaces.add(c);
        }
      }
      // 找到被代理類的父類,然後繼續查找接口信息
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }
}

到這裏就說完了mybatis插件的實現原理

整體實現其實也很簡單,我們想給那個類的哪個方法攔截一下,把實現攔截器,然後配置下要攔截的信息,之後mybatis給我們生成一個動態代理對象,就可以了。

下面我們以具體案例來更加深刻的認識理解其實現原理

加強理解

我們使用具體的案例,來走一遍

Case 1

單攔截器單攔截方法

@Intercepts(
  {@Signature(
     type= Executor.class,
     method = "update",
     args = {MappedStatement.class ,Object.class})}
)
 public class ExamplePlugin implements Interceptor {
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
       System.out.println("before execute update");
       Object returnObject = invocation.proceed();
       System.out.println("after  execute update");
       return returnObject;
     }
 }

假設目前我們就設置了這麼一個攔截器

那麼就會生成一個CachingExecutor的代理對象,我們假設代理對象的全限定名是com.sun.proxy.$Proxy0,並且這個對象對象是implements Executor

這時調用代理對象的update方法調用棧如下

mybatis-plugin

Case 2

單攔截器多攔截方法

假設有個需求需要給StatementHandlerupdate增加點一樣的攔截邏輯

@Intercepts(
  {@Signature(
     type= Executor.class,
     method = "update",
     args = {MappedStatement.class ,Object.class})},
  {@Signature(
     type= StatementHandler.class,
     method = "update",
     args = {Statement.class})}
)
 public class ExamplePlugin implements Interceptor {
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
       System.out.println("before execute update");
       Object returnObject = invocation.proceed();
       System.out.println("after  execute update");
       return returnObject;
     }
 }

同樣是獲取執行器的邏輯,這個時候我們得到的代理後的對象不會是同時實現了ExecutorStatementHandler接口的,只能實現Executor接口,因爲這個

// 這個時候type爲 CachingExecutor
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
  Set<Class<?>> interfaces = new HashSet<>();
  while (type != null) {
    // type的接口Executor顯然是在Map中的
    for (Class<?> c : type.getInterfaces()) {
      // 僅允許對接口爲Executor的實例做代理,因爲代理後的類一定得是一個Executor的實現類
      if (signatureMap.containsKey(c)) {
        interfaces.add(c);
      }
    }
    type = type.getSuperclass();
  }
  // 最終也就只能找到Executor這麼一個接口了
  return interfaces.toArray(new Class<?>[interfaces.size()]);
}

這種情況下就和上面一樣了

區別在於,如果我們需要創建一個StatementHandler的實例時,代理對象就會變成StatementHandler的實現類實例了

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  // 被攔截器增強後的代理對象StatementHandler
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

Case 3

多攔截器單攔截方法

如果有多個攔截器呢?

@Intercepts(
  {@Signature(
     type= Executor.class,
     method = "update",
     args = {MappedStatement.class ,Object.class})}
)
 public class ExamplePluginA implements Interceptor {
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
       System.out.println("before execute update");
       Object returnObject = invocation.proceed();
       System.out.println("after  execute update");
       return returnObject;
     }
 }

@Intercepts(
  {@Signature(
     type= Executor.class,
     method = "flushStatements",
     args = {})}
)
 public class ExamplePluginB implements Interceptor {
     @Override
     public Object intercept(Invocation invocation) throws Throwable {
       System.out.println("before execute flushStatements");
       Object returnObject = invocation.proceed();
       System.out.println("after  execute flushStatements");
       return returnObject;
     }
 }

上面有兩個攔截器,會出現代理對象成爲委託對象的情況,也就是對一個已經是代理對象的對象,再去爲它生成代理對象。

假設一開始生成了一個代理類類名爲$Proxy0,在InterceptorChain#pluginAll方法中,會將這個代理類作爲委託類繼續生成一個新的代理類,第一個代理類會執行ExamplePluginA攔截邏輯,第二個代理類會執行ExamplePluginB的攔截邏輯。

pluginAll方法中可以看到,位於攔截器鏈末尾的,也可以說是最後被創建的那個代理類,是最先被調用的,因爲實際返回給客戶端使用的,就是它。

假設第二個代理類類名爲$Proxy1,那麼看一下這種情況下方法的調用棧是啥樣的

mybaits-plugin-complicate

總結

這裏主要說一下mybatis爲什麼這樣實現?

  • 爲什麼不採用過濾器鏈的實現方式?

    過濾器鏈的方式,在這裏並不怎麼適用,雖然也許可以,個人觀點覺得,過濾器鏈更多的是用於過濾,它是對調用請求進行條件判斷,是否滿足某種條件進而是否具備某個繼續執行的權限。它決定的是,調用是否可以繼續進行下去。

    而攔截器,更多的是攔截調用請求,對攔截後的請求做一些額外的附加邏輯,代理其實就是這種方式,它是一種對原有邏輯的增強,不會改變最初調用的目的地,無論做了多少層的攔截處理,最終都會到達它的目的地(發生異常除外)

    如果用過濾器,需要多個過濾器鏈,並且每個類中都需要持有對其過濾器鏈的引用等等,用我淺薄的知識稍微想了下,並非不可,實而太麻煩了。最優選擇還是動態代理。

  • 爲什麼要做封裝?

    回想一下我們直接使用Jdk的動態代理,自己動手實現InvocationHandler,寫完就沒了,也就是說就做了一次增強。

    但是這裏情況更加複雜,可能有多個類的多個方法需要被增強,以Executor爲例,如果我們每個方法都要增強,並且每個方法的增強邏輯都不一樣,而實際上,最終也只生成一個代理對象,那我們就要在一個InvocationHandlerinvoke方法中,大量的if-else判斷(這裏也可以優化if-else),當前調用的是哪個方法,應該執行什麼樣的邏輯。這樣在實現上是沒問題的,只不過對一個框架來說,易用性也是強有力的吸引力、競爭力。如果這些不優雅的或者用戶實現起來比較麻煩的事,框架都給封裝好,簡單易用,那麼肯定會更加受歡迎,這肯定也會是一個好的框架。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章