Mybatis 插件機制詳解

簡介

⼀般情況下,開源框架都會提供插件或其他形式的拓展點,供開發者⾃⾏拓展。這樣的好處是顯⽽易見的,⼀是增加了框架的靈活性。⼆是開發者可以結合實際需求,對框架進⾏拓展,使其能夠更好的⼯作。以 MyBatis 爲例,我們可基於 MyBatis 插件機制實現分頁、分表,監控等功能。由於插件和業務⽆關,業務也⽆法感知插件的存在。因此可以⽆感植⼊插件,在⽆形中增強功能 。

Mybatis 插件介紹

Mybatis 作爲⼀個應⽤⼴泛的優秀的 ORM 開源框架,這個框架具有強⼤的靈活性,在四⼤組件 (Executor、StatementHandler、ParameterHandler、ResultSetHandler) 處提供了簡單易⽤的插件擴展機制。Mybatis 對持久層的操作就是藉助於四⼤核⼼對象。MyBatis ⽀持⽤插件對四⼤核⼼對象進行攔截,對 mybatis 來說插件就是攔截器,⽤來增強核⼼對象的功能,增強功能本質上是藉助於底層的動態代理實現的,換句話說,MyBatis 中的四⼤對象都是代理對象。

Mybatis 可以對這四個接口中所有的方法進行攔截

就是在下面的幾個函數里面生成代理對象實現攔截的:

Mybatis 插件原理  

介紹

在四⼤對象創建的時候

1、每個創建出來的對象不是直接返回的,⽽是 interceptorChain.pluginAll(parameterHandler) 獲取到所有的  Interceptor (攔截器)(插件需要實現的接⼝);

2、調⽤ interceptor.plugin(target),返回 target 包裝後的對象(代理對象) ;

3、插件機制,我們可以使⽤插件爲⽬標對象創建⼀個代理對象;AOP (⾯向切⾯) 我們的插件可以爲四⼤對象創建出代理對象,代理對象就可以攔截到四⼤對象的每⼀個方法執行;

攔截  

插件具體是如何攔截並附加額外的功能的呢?以 ParameterHandler 來說

 public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }
public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
}

  interceptorChain 保存了所有的攔截器 (interceptors),是 mybatis 初始化的時候創建的。調⽤攔截器鏈中的攔截器依次的對⽬標進⾏攔截或增強。interceptor.plugin(target) 中的 target 就可以理解爲 mybatis 中的四⼤對象。返回的 target 是被重重代理後的對象。

如果我們想要攔截 Executor 的 query ⽅法,那麼可以這樣定義插件:  

@Intercepts({
 @Signature(
 type = Executor.class,
 method = "query",
 args=
{MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
 )
})
public class ExeunplePlugin implements Interceptor {
 //省略邏輯
}

除此之外,我們還需將插件配置到 sqlMapConfig.xm l 中。  

<plugins>
 <plugin interceptor="com.bcst.plugin.ExamplePlugin">
 </plugin>
</plugins>

這樣 MyBatis 在啓動時可以加載插件,並保存插件實例到相關對象 (InterceptorChain,攔截器鏈) 中。待準備⼯作做完後,MyBatis 處於就緒狀態。我們在執行 SQL 時,需要先通過 DefaultSqlSessionFactory 創建 SqlSession。Executor 實例會在創建 SqlSession 的過程中被創建, Executor 實例創建完畢後, MyBatis 會通過 JDK 動態代理爲實例⽣成代理類。這樣,插件邏輯即可在 Executor 相關⽅法被調⽤前執行。以上就是 MyBatis 插件機制的基本原理。 

                                                           

                                                     

自定義插件

插件接口

Mybatis 插件接⼝ Interceptor

自定義插件

設計實現⼀個⾃定義插件  

package com.bcst.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
/*
 *@description 歡迎關注公衆號 編程識堂,每日更新技術相關文章,關注我不迷路
 *@author 小堂
 *@create 2023/7/16 21:54
 */
@Intercepts ({//注意看這個⼤花括號,也就這說這⾥可以定義多個@Signature對多個地⽅攔截,都⽤這個攔截器
@Signature (type = StatementHandler.class, //這是指攔截哪個接⼝
        method = "prepare",//這個接⼝內的哪個⽅法名,不要拼錯了
        args = { Connection.class, Integer.class}),//// 這是攔截的⽅法的⼊參,按順序寫到這,不要多也不要少,如果⽅法重載,可是要通過⽅法名和⼊參來確定唯⼀的
        })
public class MyPlugin implements Interceptor {
    /**
     * 攔截方法:只要被攔截的目標對象的目標方法被執行時,每次都會執行intercept方法
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyPlugin插件 目標方法被增強。。。。");
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        // 將執行權交給下一個攔截器
        return invocation.proceed();
    }
    /**
     * 主要爲了把當前的攔截器生成代理保存到攔截鏈中
     * @Description 包裝⽬標對象 爲⽬標對象創建代理對象
     * @Param  target爲要攔截的對象
     * @Return 代理對象
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("MyPlugin插件將要包裝的⽬標對象:"+target);
        Object wrap = Plugin.wrap(target, this);
        return wrap;
    }
    /**
     * 獲取配置文件的參數
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("獲取配置文件的參數是:"+properties);
    }
}

sqlMapConfig.xml

 <plugins>
        <plugin interceptor="com.bcst.plugin.MyPlugin">
            <property />
        </plugin>
 </plugins>

測試

源碼分析

執行插件邏輯

首先從源頭 -> 配置文件開始分析:

XMLConfigBuilder 解析 MyBatis 全局配置文件的 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.addInterceptor(interceptorInstance);
        }
    }
}

具體的解析代碼其實比較簡單,就不貼了,主要就是通過反射實例化 plugin 節點中的 interceptor 屬性表示的類。然後調用全局配置類 Configuration 的 addInterceptor 方法。

public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

這個 interceptorChain 是 Configuration 的內部屬性,類型爲 InterceptorChain,也就是一個攔截器鏈,我們來看下它的定義:

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }
    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }
}

現在我們理解了攔截器配置的解析以及攔截器的歸屬,現在我們回過頭看下爲何攔截器會攔截這些方法(Executor,ParameterHandler,ResultSetHandler,StatementHandler 的部分方法):

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}
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) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    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, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;
    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();
    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }
    this.boundSql = boundSql;
    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

以上 4 個方法都是 Configuration 的方法。這些方法在 MyBatis 的一個操作 (新增,刪除,修改,查詢) 中都會被執行到,執行的先後順序是 Executor,ParameterHandler,ResultSetHandler,StatementHandler(其中 ParameterHandler 和 ResultSetHandler 的創建是在創建 StatementHandler[3 個可用的實現類 CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler] 的時候,其構造函數調用的 [這 3 個實現類的構造函數其實都調用了父類 BaseStatementHandler 的構造函數])。

這 4 個方法實例化了對應的對象之後,都會調用 interceptorChain 的 pluginAll 方法,InterceptorChain 的 pluginAll 剛纔已經介紹過了,就是遍歷所有的攔截器,然後調用各個攔截器的 plugin 方法。

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;
  }
}}

注意:攔截器的 plugin 方法的返回值會直接被賦值給原先的對象。

由於可以攔截 StatementHandler,這個接口主要處理 sql 語法的構建,因此比如分頁的功能,可以用攔截器實現,只需要在攔截器的 plugin 方法中處理 StatementHandler 接口實現類中的 sql 即可,可使用反射實現。

MyBatis 還提供了 @Intercepts 和 @Signature 關於攔截器的註解。官網的例子就是使用了這 2 個註解,還包括了 Plugin 類的使用:

public interface Interceptor {
  Object intercept(Invocation invocation) throws Throwable;
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
}

代理鏈的生成

Mybatis 支持對 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler 進行攔截,也就是說會對這 4 種對象進行代理。通過查看 Configuration 類的源代碼我們可以看到,每次都對目標對象進行代理鏈的生成。

下面以 Executor 爲例。Mybatis 在創建 Executor 對象時會執行下面一行代碼:

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

InterceptorChain 裏保存了所有的攔截器,它在 mybatis 初始化的時候創建。上面這句代碼的含義是調用攔截器鏈裏的每個攔截器依次對 executor 進行 plugin(生成代理對象)代碼如下:

/** 
  * 每一個攔截器對目標類都進行一次代理 
  * @param target 
  * @return 層層代理後的對象 
  */  
 public Object pluginAll(Object target) {  
     for(Interceptor interceptor : interceptors) {  
         target= interceptor.plugin(target);  
     }  
     return target;  
 }

下面以一個簡單的例子來看看這個 plugin 方法裏到底發生了什麼:

@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 {  
        return invocation.proceed();  
    }  
    @Override  
    public Object plugin(Object target) {  
        return Plugin.wrap(target, this);  
    }  
    @Override  
    public void setProperties(Properties properties) {  
    }
}

每一個攔截器都必須實現上面的三個方法,其中:

註解裏描述的是指定攔截方法的簽名 [type,method,args] (即對哪種對象的哪種方法進行攔截),它在攔截前用於決斷。

定義自己的 Interceptor 最重要的是要實現 plugin 方法和 intercept 方法,在 plugin 方法中我們可以決定是否要進行攔截進而決定要返回一個什麼樣的目標對象。而 intercept 方法就是要進行攔截的時候要執行的方法。

對於 plugin 方法而言,其實 Mybatis 已經爲我們提供了一個實現。Mybatis 中有一個叫做 Plugin 的類,裏面有一個靜態方法 wrap(Object target,Interceptor interceptor),通過該方法可以決定要返回的對象是目標對象還是對應的代理。這裏我們先來看一下 Plugin 的源碼:

package org.apache.ibatis.plugin;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.ibatis.reflection.ExceptionUtil;
//這個類是Mybatis攔截器的核心,大家可以看到該類繼承了InvocationHandler
//又是JDK動態代理機制
public class Plugin implements InvocationHandler {
  //目標對象
  private Object target;
  //攔截器
  private Interceptor interceptor;
  //記錄需要被攔截的類與方法
  private 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) {
    //首先根據interceptor上面定義的註解 獲取需要攔截的信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //目標對象的Class
    Class<?> type = target.getClass();
    //返回需要攔截的接口信息
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //如果長度爲>0 則返回代理類 否則不做處理
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
  //代理對象每次調用的方法
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //通過method參數定義的類 去signatureMap當中查詢需要攔截的方法集合
      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);
    }
  }
  //根據攔截器接口(Interceptor)實現類上面的註解獲取相關信息
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //獲取註解信息
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    //爲空則拋出異常
    if (interceptsAnnotation == null) { // issue #251
      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) {
      //根據Signature註解定義的type信息去signatureMap當中查詢需要攔截方法的集合
      Set<Method> methods = signatureMap.get(sig.type());
      //第一次肯定爲null 就創建一個並放入signatureMap
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //找到sig.type當中定義的方法 並加入到集合
        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;
  }
  //根據對象類型與signatureMap獲取接口信息
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    //循環type類型的接口信息 如果該類型存在與signatureMap當中則加入到set當中去
    while (type != null) {
        //type:Executor接口實現類,比如SimpleExecutor
        //type.getInterfaces():例如org.apache.ibatis.executor.Executor
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    //轉換爲數組返回
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }
}

下面是倆個註解類的定義源碼:

package org.apache.ibatis.plugin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  Signature[] value();
}
package org.apache.ibatis.plugin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Signature {
  Class<?> type();
  String method();
  Class<?>[] args();
}

Plugin.wrap 方法

從前面可以看出,每個攔截器的 plugin 方法是通過調用 Plugin.wrap 方法來實現的。代碼如下:

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) {  
        // 生成代理對象, Plugin對象爲該代理對象的InvocationHandler  (InvocationHandler屬於java代理的一個重要概念,不熟悉的請參考相關概念)  
        return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target,interceptor,signatureMap));  
    }  
    return target;  
}

這個 Plugin 類有三個屬性:

private Object target;// 被代理的目標類
private Interceptor interceptor;// 對應的攔截器
private Map<Class<?>, Set<Method>> signatureMap;// 攔截器攔截的方法緩存

getSignatureMap 方法

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) { // issue #251
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        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;
}

getSignatureMap 方法解釋:首先會拿到攔截器這個類的 @Interceptors 註解,然後拿到這個註解的屬性 @Signature 註解集合,然後遍歷這個集合,遍歷的時候拿出 @Signature 註解的 type 屬性 (Class 類型),然後根據這個 type 得到帶有 method 屬性和 args 屬性的 Method。由於 @Interceptors 註解的 @Signature 屬性是一個屬性,所以最終會返回一個以 type 爲 key,value 爲 Set 的 Map。

@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})

比如這個 @Interceptors 註解會返回一個 key 爲 Executor,value 爲集合 (這個集合只有一個元素,也就是 Method 實例,這個 Method 實例就是 Executor 接口的 update 方法,且這個方法帶有 MappedStatement 和 Object 類型的參數)。這個 Method 實例是根據 @Signature 的 method 和 args 屬性得到的。如果 args 參數跟 type 類型的 method 方法對應不上,那麼將會拋出異常。

getAllInterfaces 方法

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()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}

getAllInterfaces 方法解釋:根據目標實例 target(這個 target 就是之前所說的 MyBatis 攔截器可以攔截的類,Executor,ParameterHandler,ResultSetHandler,StatementHandler) 和它的父類們,返回 signatureMap 中含有 target 實現的接口數組。

所以 Plugin 這個類的作用就是根據 @Interceptors 註解,得到這個註解的屬性 @Signature 數組,然後根據每個 @Signature 註解的 type,method,args 屬性使用反射找到對應的 Method。最終根據調用的 target 對象實現的接口決定是否返回一個代理對象替代原先的 target 對象。

我們再次結合 (Executor)interceptorChain.pluginAll(executor) 這個語句來看,這個語句內部對 executor 執行了多次 plugin, 第一次 plugin 後通過 Plugin.wrap 方法生成了第一個代理類,姑且就叫 executorProxy1,這個代理類的 target 屬性是該 executor 對象。第二次 plugin 後通過 Plugin.wrap 方法生成了第二個代理類,姑且叫 executorProxy2,這個代理類的 target 屬性是 executorProxy1... 這樣通過每個代理類的 target 屬性就構成了一個代理鏈(從最後一個 executorProxyN 往前查找,通過 target 屬性可以找到最原始的 executor 類)。

代理鏈上的攔截

代理鏈生成後,對原始目標的方法調用都轉移到代理者的 invoke 方法上來了。Plugin 作爲 InvocationHandler 的實現類,他的 invoke 方法是怎麼樣的呢?

比如 MyBatis 官網的例子,當 Configuration 調用 newExecutor 方法的時候,由於 Executor 接口的 update(MappedStatement ms, Object parameter) 方法被攔截器被截獲。因此最終返回的是一個代理類 Plugin,而不是 Executor。這樣調用方法的時候,如果是個代理類,那麼會執行:

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);  
    }  
}

沒錯,如果找到對應的方法被代理之後,那麼會執行 Interceptor 接口的 interceptor 方法。

在 invoke 裏,如果方法簽名和攔截中的簽名一致,就調用攔截器的攔截方法。我們看到傳遞給攔截器的是一個 Invocation 對象,這個對象是什麼樣子的,他的功能又是什麼呢?

public class Invocation {  
    private Object target;  
    private Method method;  
    private Object[] args;  
    public Invocation(Object target, Method method, Object[] args) {  
        this.target =target;  
        this.method =method;  
        this.args =args;  
    }  
    ...  
    public Object proceed() throws InvocationTargetException, IllegalAccessException {  
        return method.invoke(target, args);  
    }  
}

可以看到,Invocation 類保存了代理對象的目標類,執行的目標類方法以及傳遞給它的參數。

在每個攔截器的 intercept 方法內,最後一個語句一定是 return invocation.proceed()(不這麼做的話攔截器鏈就斷了,你的 mybatis 基本上就不能正常工作了)。invocation.proceed() 只是簡單的調用了下 target 的對應方法,如果 target 還是個代理,就又回到了上面的 Plugin.invoke 方法了。這樣就形成了攔截器的調用鏈推進。

public Object intercept(Invocation invocation) throws Throwable {  
    //完成代理類本身的邏輯  
    ...
    //通過invocation.proceed()方法完成調用鏈的推進
    return invocation.proceed();
}

總結

MyBatis 攔截器接口提供的 3 個方法中,plugin 方法用於某些處理器 (Handler) 的構建過程。interceptor 方法用於處理代理類的執行。setProperties 方法用於攔截器屬性的設置。

其實 MyBatis 官網提供的使用 @Interceptors 和 @Signature 註解以及 Plugin 類這樣處理攔截器的方法,我們不一定要直接這樣使用。我們也可以拋棄這 3 個類,直接在 plugin 方法內部根據 target 實例的類型做相應的操作。

總體來說 MyBatis 攔截器還是很簡單的,攔截器本身不需要太多的知識點,但是學習攔截器需要對 MyBatis 中的各個接口很熟悉,因爲攔截器涉及到了各個接口的知識點。

我們假設在 MyBatis 配置了一個插件,在運行時會發生什麼?

因此,在編寫插件時需注意以下幾個原則:

pageHelper 分頁插件

MyBatis 可以使⽤第三⽅的插件來對功能進⾏擴展,分⻚助⼿ PageHelper 是將分⻚的複雜操作進⾏封 裝,使⽤簡單的⽅式即可獲得分⻚的相關數據。

開發步驟: 

① 導⼊通⽤ PageHelper 的座標

② 在 mybatis 核⼼配置⽂件中配置 PageHelper 插件

③ 測試分⻚數據獲取  

導入通用 PageHelper 座標

<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper</artifactId>
  <version>3.7.5</version>
</dependency>
<dependency>
  <groupId>com.github.jsqlparser</groupId>
  <artifactId>jsqlparser</artifactId>
  <version>0.9.1</version>
</dependency>

在 mybatis 核心配置⽂件中配置 PageHelper 插件

<!--注意:分⻚助⼿的插件 配置在通⽤館mapper之前*-->
<plugin interceptor="com.github.pagehelper.PageHelper">
  <!--指定⽅⾔-->
  <property />
</plugin>

測試分頁代碼實現

public void test6() throws IOException {
        //加載核⼼配置⽂件
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        //獲得sqlSession⼯⼚對象
        SqlSessionFactory sqlSessionFactory = new
                SqlSessionFactoryBuilder().build(resourceAsStream);
        //獲得sqlSession對象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IUsrDao usrDao = sqlSession.getMapper(IUsrDao.class);
        PageHelper.startPage(1,20);
        List<User> users = usrDao.findAll();
        PageInfo<User> pageInfo = new PageInfo<>(users);
        System.out.println(pageInfo.toString());
 }

自定義 PageInterceptor 分頁插件

介紹

Mybatis 的分頁功能很弱,它是基於內存的分頁(查出所有記錄再按偏移量和 limit 取結果),在大數據量的情況下這樣的分頁基本上是沒有用的。本文基於插件,通過攔截 StatementHandler 重寫 sql 語句,實現數據庫的物理分頁。

爲什麼在 StatementHandler 攔截

在前面章節介紹了一次 sqlsession 的完整執行過程,從中可以知道 sql 的解析是在 StatementHandler 裏完成的,所以爲了重寫 sql 需要攔截 StatementHandler。

MetaObject 簡介

在實現裏大量使用了 MetaObject 這個對象,因此有必要先介紹下它。MetaObject 是 Mybatis 提供的一個的工具類,通過它包裝一個對象後可以獲取或設置該對象的原本不可訪問的屬性(比如那些私有屬性)。它有個三個重要方法經常用到:

intercept 實現

package com.bcst.plugin;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
/**
  分頁插件
 *@description 歡迎關注公衆號 編程識堂,每日更新技術相關文章,關注我不迷路
 *@author 小堂
 *@create 2023/7/20 20:54
 */
@Intercepts({@Signature(
        method = "prepare",
        type = StatementHandler.class,
        args = {Connection.class, Integer.class}
)})
public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("PageInterceptor插件 目標方法被增強。。。。");
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        // 分離代理對象鏈(由於目標類可能被多個攔截器攔截,從而形成多次代理,通過下面的兩次循環
        // 可以分離出最原始的的目標類)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler =   SystemMetaObject.forObject(object);
        }
        // 分離最後一個代理對象的目標類
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler =   SystemMetaObject.forObject(object);
        }
        RowBounds rowBounds = null;
        try {
            rowBounds = (RowBounds)metaStatementHandler.getValue("delegate.rowBounds");
        } catch (Exception var11) {
            return invocation.proceed();
        }
        if (rowBounds != null && rowBounds != RowBounds.DEFAULT) {
            String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
            Connection connection = (Connection) invocation.getArgs()[0];
            MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
            BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
            int totalCount = this.getPageTotalCount(originalSql, connection, mappedStatement, boundSql);
            if (totalCount > 0) {
                String sql = "";
                if (rowBounds.getOffset() > 0) {
                    sql = originalSql + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
                } else {
                    sql = originalSql + " limit " + rowBounds.getLimit();
                }
                // 採用物理分頁後,就不需要mybatis的內存分頁了,所以重置下面的兩個參數
                metaStatementHandler.setValue("delegate.boundSql.sql", sql);
                metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
                metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
            }
            metaStatementHandler.setValue("delegate.boundSql.parameterObject.totalCount", totalCount);
        }
        // 將執行權交給下一個攔截器
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        //當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標本身,減少目標被代理的
        if (target instanceof StatementHandler){
            System.out.println("PageInterceptor插件將要包裝的⽬標對象:"+target);
            return Plugin.wrap(target,this);
        }
        return target;
    }
    @Override
    public void setProperties(Properties properties) {
    }
    private int getPageTotalCount(String sql, Connection connection, MappedStatement mappedStatement, BoundSql boundSql) {
        String countSql = "select count(0) from (" + sql + ") as total";
        PreparedStatement countStmt = null;
        ResultSet rs = null;
        int totalCount = 0;
        try {
            countStmt = connection.prepareStatement(countSql);
            BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
            additionalParameter(boundSql,countBS);
            this.setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
            rs = countStmt.executeQuery();
            if (rs.next()) {
                totalCount = rs.getInt(1);
            }
        } catch (SQLException var22) {
        } finally {
            try {
                rs.close();
            } catch (SQLException var21) {
                var21.printStackTrace();
            }
            try {
                countStmt.close();
            } catch (SQLException var20) {
                var20.printStackTrace();
            }
        }
        return totalCount;
    }
    private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException {
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler.setParameters(ps);
    }
    private void additionalParameter(BoundSql boundSql,BoundSql targetBoundSql){
        Class c = boundSql.getClass();
        try {
            Field field = c.getDeclaredField("additionalParameters");
            field.setAccessible(true);
            Map<String,Object> boundMap = (Map<String,Object>)field.get(boundSql);
            if(boundMap!=null) {
                for (Map.Entry<String, Object> entry : boundMap.entrySet()) {
                    targetBoundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

StatementHandler 的默認實現類是 RoutingStatementHandler,因此攔截的實際對象是它。RoutingStatementHandler 的主要功能是分發,它根據配置 Statement 類型創建真正執行數據庫操作的 StatementHandler,並將其保存到 delegate 屬性裏。由於 delegate 是一個私有屬性並且沒有提供訪問它的方法,因此需要藉助 MetaObject 的幫忙。通過 MetaObject 的封裝後我們可以輕易的獲得想要的屬性。

通過他們可以分離出原始的 RoutingStatementHandler(而不是代理對象)。

在 mybatis 核心配置文件中配置 PageInterceptor 插件  

<plugins>
        <plugin interceptor="com.bcst.plugin.MyPlugin">
            <property />
        </plugin>
        <!--注意:分⻚助⼿的插件 配置在通⽤館mapper之前*-->
        <plugin interceptor="com.bcst.plugin.PageInterceptor">
            <!--指定⽅⾔-->
            <property />
        </plugin>
</plugins>

測試

PagingListBo

package com.bcst.pojo;
/**
  分頁請求體
 *@description 歡迎關注公衆號 編程識堂,每日更新技術相關文章,關注我不迷路
 *@author 小堂
 *@create 2023/7/20 20:54
 */
public class PagingListBo {
    /**
     * 偏移量
     */
    private Integer offset=0;
    /**
     * 限量每頁多少條(不填默認10
     */
    private Integer limit=10;
    /**
     * 總數量
     */
    private Integer totalCount=0;
    //總頁數
    private Integer pageCount=0;
    /**
     * 頁碼當前多少頁(不填默認1)
     */
    private Integer  pageIndex=1;
    public Integer getOffset() {
        offset = limit * (this.pageIndex-1);
        return offset;
    }
    public void setOffset(Integer offset) {
        this.offset = offset;
    }
    public Integer getLimit() {
        return limit;
    }
    public void setLimit(Integer limit) {
        this.limit = limit;
    }
    public Integer getTotalCount() {
        return totalCount;
    }
    public void setTotalCount(Integer totalCount) {
        this.totalCount = totalCount;
    }
    public Integer getPageCount() {
        pageCount= (this.totalCount + this.limit - 1) / this.limit;
        return pageCount;
    }
    public void setPageCount(Integer pageCount) {
        this.pageCount = pageCount;
    }
    public Integer getPageIndex() {
        return  pageIndex;
    }
    public void setPageIndex(Integer pageIndex) {
        this. pageIndex = pageIndex;
    }
}

UserQueryBo

/**
  用戶請求體
 *@description 歡迎關注公衆號 編程識堂,每日更新技術相關文章,關注我不迷路
 *@author 小堂
 *@create 2023/7/20 20:54
 */
public class UserQueryBo extends PagingListBo{
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

PageVO

package com.bcst.pojo;
import java.util.List;
/**
  分頁返回體
 *@description 歡迎關注公衆號 編程識堂,每日更新技術相關文章,關注我不迷路
 *@author 小堂
 *@create 2023/7/20 20:54
 */
public class PageVO<T>{
    /**
     * 分頁總數
     */
    private long total;
    /**
     * 數據集
     */
    private List<T> rows;
    public PageVO() {
    }
    public PageVO(long total, List<T> rows) {
        this.total = total;
        this.rows = rows;
    }
    public long getTotal() {
        return total;
    }
    public void setTotal(long total) {
        this.total = total;
    }
    public List<T> getRows() {
        return rows;
    }
    public void setRows(List<T> rows) {
        this.rows = rows;
    }
}

IUsrDao

/*
 *@description 歡迎關注公衆號 編程識堂,每日更新技術相關文章,關注我不迷路
 *@author 小堂
 *@create 2023/7/9 11:58
 */
public interface IUsrDao {
    List<User> findList( UserQueryBo userQueryBo, RowBounds rowBounds);
}

UserMapper.xml

<select resultType="com.bcst.pojo.User">
        select  username name,phone from user where username like concat('%',#{name},'%')
</select>

sqlMapConfig.xml

<plugins>
        <plugin interceptor="com.bcst.plugin.MyPlugin">
            <property />
        </plugin>
        <!--注意:分⻚助⼿的插件 配置在通⽤館mapper之前*-->
        <plugin interceptor="com.bcst.plugin.PageInterceptor">
            <!--指定⽅⾔-->
            <property />
        </plugin>
    </plugins>

Test

 public void test7() throws IOException {
        //加載核⼼配置⽂件
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        //獲得sqlSession⼯⼚對象
        SqlSessionFactory sqlSessionFactory = new
                SqlSessionFactoryBuilder().build(resourceAsStream);
        //獲得sqlSession對象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        IUsrDao usrDao = sqlSession.getMapper(IUsrDao.class);
        UserQueryBo userQueryBo = new UserQueryBo();
        userQueryBo.setName("u1105");
        RowBounds rowBounds = new RowBounds(1,20);
        List<User> users = usrDao.findList(userQueryBo,rowBounds);
        PageVO<User> pageVO = new PageVO<>(userQueryBo.getTotalCount(),users);
        System.out.println("總條數:"+pageVO.getTotal()+"當前頁條數:"+pageVO.getRows().size());
    }

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Kx_ceP1eu3dtY5xht7ffqA