送命題:講一講Mybatis外掛的原理及如何實現?

愛撒謊的男孩發表於2020-09-17

持續原創輸出,點選上方藍字關注我吧

目錄

  • 前言
  • 環境配置
  • 什麼是外掛?
  • 如何自定義外掛?
    • 舉個例子
    • 用到哪些註解?
    • 如何注入Mybatis?
    • 測試
  • 外掛原理分析
    • 如何生成代理物件?
    • 如何執行?
    • 總結
  • 分頁外掛的原理分析
  • 總結

前言

  • Mybatis的分頁外掛相信大家都使用過,那麼可知道其中的實現原理?分頁外掛就是利用的Mybatis中的外掛機制實現的,在Executorquery執行前後進行分頁處理。
  • 此篇文章就來介紹以下Mybatis的外掛機制以及在底層是如何實現的。

環境配置

  • 本篇文章講的一切內容都是基於Mybatis3.5SpringBoot-2.3.3.RELEASE

什麼是外掛?

  • 外掛是Mybatis中的最重要的功能之一,能夠對特定元件的特定方法進行增強。
  • MyBatis 允許你在對映語句執行過程中的某一點進行攔截呼叫。預設情況下,MyBatis 允許使用外掛來攔截的方法呼叫包括:
    • Executorupdate, query, flushStatements, commit, rollback, getTransaction, close, isClosed
    • ParameterHandler: getParameterObject, setParameters
    • ResultSetHandlerhandleResultSets, handleOutputParameters
    • StatementHandler: prepare, parameterize, batch, update, query

如何自定義外掛?

  • 外掛的實現其實很簡單,只需要實現Mybatis提供的Interceptor這個介面即可,原始碼如下:
public interface Interceptor {
  //攔截的方法
  Object intercept(Invocation invocation) throws Throwable;
  //返回攔截器的代理物件
  Object plugin(Object target);
  //設定一些屬性
  void setProperties(Properties properties);

}

舉個例子

  • 有這樣一個需求:需要在Mybatis執行的時候篡改selectByUserId的引數值。
  • 分析:修改SQL的入參,應該在哪個元件的哪個方法上攔截篡改呢?研究過原始碼的估計都很清楚的知道,ParameterHandler中的setParameters()方法就是對引數進行處理的。因此肯定是攔截這個方法是最合適。
  • 自定義的外掛如下:
/**
 * @Intercepts 註解標記這是一個攔截器,其中可以指定多個@Signature
 * @Signature 指定該攔截器攔截的是四大物件中的哪個方法
 *      type:攔截器的四大物件的型別
 *      method:攔截器的方法,方法名
 *      args:入參的型別,可以是多個,根據方法的引數指定,以此來區分方法的過載
 */
@Intercepts(
        {
                @Signature(type = ParameterHandler.class,method ="setParameters",args = {PreparedStatement.class})
        }
)
public class ParameterInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("攔截器執行:"+invocation.getTarget());
        //目標物件
        Object target = invocation.getTarget();
        //獲取目標物件中所有屬性的值,因為ParameterHandler使用的是DefaultParameterHandler,因此裡面的所有的屬性都封裝在其中
        MetaObject metaObject = SystemMetaObject.forObject(target);
        //使用xxx.xxx.xx的方式可以層層獲取屬性值,這裡獲取的是mappedStatement中的id值
        String value = (String) metaObject.getValue("mappedStatement.id");
        //如果是指定的查詢方法
        if ("cn.cb.demo.dao.UserMapper.selectByUserId".equals(value)){
            //設定引數的值是admin_1,即是設定id=admin_1,因為這裡只有一個引數,可以這麼設定,如果有多個需要需要迴圈
            metaObject.setValue("parameterObject", "admin_1");
        }
        //執行目標方法
        return invocation.proceed();
    }


    @Override
    public Object plugin(Object target) {
        //如果沒有特殊定製,直接使用Plugin這個工具類返回一個代理物件即可
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
  • intercept方法:最終會攔截的方法,最重要的一個方法。
  • plugin方法:返回一個代理物件,如果沒有特殊要求,直接使用Mybatis的工具類Plugin返回即可。
  • setProperties:設定一些屬性,不重要。

用到哪些註解?

  • 自定義外掛需要用到兩個註解,分別是@Intercepts@Signature
  • @Intercepts:標註在實現類上,表示這個類是一個外掛的實現類。
  • @Signature:作為@Intercepts的屬性,表示需要增強Mybatis的某些元件中的某些方法(可以指定多個)。常用的屬性如下:
    • Class<?> type():指定哪個元件(ExecutorParameterHandlerResultSetHandlerStatementHandler
    • String method():指定增強元件中的哪個方法,直接寫方法名稱。
    • Class<?>[] args():方法中的引數,必須一一對應,可以寫多個;這個屬性非常重用,區分過載方法。

如何注入Mybatis?

  • 上面已經將外掛定義好了,那麼如何注入到Mybatis中使其生效呢?

  • 前提:由於本篇文章的環境是SpringBoot+Mybatis,因此講一講如何在SpringBoot中將外掛注入到Mybatis中。

  • 在Mybatis的自動配置類MybatisAutoConfiguration中,注入SqlSessionFactory的時候,有如下一段程式碼:

  • 上圖中的this.interceptors是什麼,從何而來,其實就是從容器中的獲取的Interceptor[],如下一段程式碼: 2

  • 從上圖我們知道,這外掛最終還是從IOC容器中獲取的Interceptor[]這個Bean,因此我們只需要在配置類中注入這個Bean即可,如下程式碼:

/**
 * @Configuration:這個註解標註該類是一個配置類
 */
@Configuration
public class MybatisConfig{

    /**
     * @Bean : 該註解用於向容器中注入一個Bean
     * 注入Interceptor[]這個Bean
     * @return
     */
    @Bean
    public Interceptor[] interceptors(){
        //建立ParameterInterceptor這個外掛
        ParameterInterceptor parameterInterceptor = new ParameterInterceptor();
        //放入陣列返回
        return new Interceptor[]{parameterInterceptor};
    }
}

測試

  • 此時自定義的外掛已經注入了Mybatis中了,現在測試看看能不能成功執行呢?測試程式碼如下:
    @Test
    void contextLoads() {
      //傳入的是1222
        UserInfo userInfo = userMapper.selectByUserId("1222");
        System.out.println(userInfo);

    }
  • 測試程式碼傳入的是1222,由於外掛改變了入參,因此查詢出來的應該是admin_1這個人。

外掛原理分析

  • 外掛的原理其實很簡單,就是在建立元件的時候生成代理物件(Plugin),執行元件方法的時候攔截即可。下面就來詳細介紹一下外掛在Mybatis底層是如何工作的?
  • Mybatis的四大元件都是在Mybatis的配置類Configuration中建立的,具體的方法如下:

//建立Executor
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);
    }
    //呼叫pluginAll方法,生成代理物件
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
  
  //建立ParameterHandler
  public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    //呼叫pluginAll方法,生成代理物件
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

//建立ResultSetHandler
  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);
    //呼叫pluginAll方法,生成代理物件
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }
  
  //建立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);
    //呼叫pluginAll方法,生成代理物件
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
  • 從上面的原始碼可以知道,建立四大元件的方法中都會執行pluginAll()這個方法來生成一個代理物件。具體如何生成的,下面詳解。

如何生成代理物件?

  • 建立四大元件過程中都執行了pluginAll()這個方法,此方法原始碼如下:
public Object pluginAll(Object target) {
    //迴圈遍歷外掛
    for (Interceptor interceptor : interceptors) {
      //呼叫外掛的plugin()方法
      target = interceptor.plugin(target);
    }
    //返回
    return target;
  }
  • pluginAll()方法很簡單,直接迴圈呼叫外掛的plugin()方法,但是我們呼叫的是Plugin.wrap(target, this)這行程式碼,因此要看一下wrap()這個方法的原始碼,如下:
public static Object wrap(Object target, Interceptor interceptor) {
    //獲取註解的@signature的定義
    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;
  }
  • Plugin.wrap()這個方法的邏輯很簡單,判斷這個外掛是否是攔截對應的元件,如果攔截了,生成代理物件(Plugin)返回,沒有攔截直接返回,上面例子中生成的代理物件如下圖:

如何執行?

  • 上面講了Mybatis啟動的時候如何根據外掛生成代理物件的(Plugin)。現在就來看看這個代理物件是如何執行的?
  • 既然是動態代理,肯定會執行的invoke()這個方法,Plugin類中的invoke()原始碼如下:
@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //獲取@signature標註的方法
      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);
    }
  }
  • 邏輯很簡單,這個方法被攔截了就執行外掛的intercept()方法,沒有被攔截,則執行原方法。
  • 還是以上面自定義的外掛來看看執行的流程:
    • setParameters()這個方法在PreparedStatementHandler中被呼叫,如下圖:
    • 執行invoke()方法,發現setParameters()這個方法被攔截了,因此直接執行的是intercept()方法。

總結

  • Mybatis中外掛的原理其實很簡單,分為以下幾步:
    1. 在專案啟動的時候判斷元件是否有被攔截,如果沒有直接返回原物件。
    2. 如果有被攔截,返回動態代理的物件(Plugin)。
    3. 執行到的元件的中的方法時,如果不是代理物件,直接執行原方法
    4. 如果是代理物件,執行Plugininvoke()方法。

分頁外掛的原理分析

  • 此處安利一款經常用的分頁外掛pagehelper,Maven依賴如下:
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.6</version>
        </dependency>
  • 分頁外掛很顯然也是根據Mybatis的外掛來定製的,來看看外掛PageInterceptor的原始碼如下:
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class PageInterceptor implements Interceptor {}
  • 既然是分頁功能,肯定是在query()的時候攔截,因此肯定是在Executor這個元件中。
  • 分頁外掛的原理其實很簡單,不再一一分析原始碼了,根據的自己定義的分頁資料重新賦值RowBounds來達到分頁的目的,當然其中涉及到資料庫方言等等內容,不是本章重點,有興趣可以看一下GitHub上的文件

總結

  • 對於業務開發的程式設計師來說,外掛的這個功能很少用到,但是不用就不應該瞭解嗎?做人要有追求,哈哈。
  • 歡迎關注作者的微信公眾號碼猿技術專欄,作者為你們精心準備了springCloud最新精彩視訊教程精選500本電子書架構師免費視訊教程等等免費資源,讓我們一起進階,一起成長。

相關文章