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


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

目錄

  • 前言
  • 環境配置
  • 什么是插件?
  • 如何自定義插件?
    • 舉個栗子
    • 用到哪些注解?
    • 如何注入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本電子書架構師免費視頻教程等等免費資源,讓我們一起進階,一起成長。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM