天天看點

MyBatis(八):MyBatis插件機制詳解

  1. MyBatis插件插件機制簡介

    ​ MyBatis插件其實就是為使用者提供的自行拓展攔截器,主要是為了可以更好的滿足業務需要。

    ​ 在MyBatis中提供了四大核心元件對資料庫進行處理,分别是Executor、Statement Handler、ParameterHandler及ResultSetHandler,同時也支援對這四大元件進行自定義擴充攔截,用來增強核心對象的功能。其本質上是使用底層的動态代理來實作的,即程式運作時執行的都是代理後的對象。

    MyBatis允許攔截的方法如下:
    • 執行器Executor (update、query、commit、rollback等方法);
    • SQL文法建構器StatementHandler (prepare、parameterize、batch、updates query等方 法);
    • 參數處理器ParameterHandler (getParameterObject、setParameters方法);
    • 結果集處理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);
  2. 攔截器參數簡介
    • 攔截器注解
      @Intercepts(//可配置多個@Signature,均使用此類進行攔截增強
        {
              @Signature(//指定需要攔截的類、方法及方法參數
                type = Executor.class,//需要攔截接口
                method = "update",//需要攔截方法名稱
                args = {MappedStatement.class, Object.class}//攔截方法的請求參數
              )
      	}
      )
                 
    • 實作Interceptor接口,并實作方法
      public Object intercept(Invocation invocation)//每次攔截到都會執行此方法,方法内寫增強邏輯
        invocation//代理對象,可以擷取目标方法、請求參數、執行結果等
        invocation.proceed() //執行目标方法
                 
      public Object plugin(Object target) 
        Plugin.wrap(target,this)//包裝目标對象,為目标對象建立代理對象,将目前生成的代理對象放入攔截器鍊中
                 
      public void setProperties(Properties properties)//擷取配置檔案中的插件參數,插件初始化時調用一次
                 
  3. 自定義插件
    • 建立MyExecuter類,實作Interceptor接口
      package com.rangers.plugin;
      
      import org.apache.ibatis.executor.Executor;
      import org.apache.ibatis.mapping.MappedStatement;
      import org.apache.ibatis.plugin.*;
      
      import java.lang.reflect.Method;
      import java.util.Properties;
      
      /**
       * @Author Rangers
       * @Description 自定義Executor update方法
       * @Date 2021-03-11
       **/
      @Intercepts({
              @Signature(type = Executor.class,
                          method = "update",
                          args = {MappedStatement.class, Object.class}
              )
      })
      public class MyExecuter implements Interceptor {
      
          // 接收插件的配置參數
          private Properties properties = new Properties();
      
          // 增強邏輯寫在此方法中
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              // 列印插件的配置參數
              System.out.println("插件的配置參數:"+properties.toString());
              // 擷取目标方法Method對象
              Method method = invocation.getMethod();
              // 擷取目标方法的請求參數 與args清單一一對應
              String reqParam = invocation.getArgs()[1].toString();
              System.out.println("方法名稱:"+method.getName()+" 請求參數:"+ reqParam);
              // 執行目标方法
              Object result = invocation.proceed();
              System.out.println("方法名稱:"+method.getName()+" 執行結果:"+result);
              return result;
          }
      
          /**
           * @Author Rangers
           * @Description 
           **/
          @Override
          public Object plugin(Object target) {
              //System.out.println("需要包裝的目标對象:"+target+" 目标對象類型"+ target.getClass());
              // 主要是将目前生成的代理對象放入攔截器鍊中,包裝目标對象,為目标對象建立代理對象
              return Plugin.wrap(target,this);
          }
      
          /**
           * @Author Rangers
           * @Description 擷取配置檔案中的插件屬性參數,插件初始化時調用一次
           **/
          @Override
          public void setProperties(Properties properties) {
              // 将配置參數進行接收
              this.properties = properties;
          }
      }
                 
    • 主配置檔案添加标簽
      <plugins>
        	<!--指定攔截器類-->
          <plugin interceptor="com.rangers.plugin.MyExecuter">
            	<!--配置攔截器 屬性參數-->
              <property name="param1" value="value1"/>
              <property name="param2" value="value2"/>
              <property name="param3" value="value3"/>
          </plugin>
      </plugins>
                 
  4. 插件原理

    a、在Executor、StatementHandler、ParameterHandler及ResultSetHandler四大對象建立時,并不是直接傳回的,而是中間多了一步interceptorChain.pluginAll()(均在Configuration類中進行建立)。

    • Executor—interceptorChain.pluginAll(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);
      }
      executor = (Executor) interceptorChain.pluginAll(executor);
      return executor;
    }
               
    • StatementHandler—interceptorChain.pluginAll(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) interceptorChain.pluginAll(statementHandler);
      return statementHandler;
    }
               
    • ParameterHandler—interceptorChain.pluginAll(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;
    }
               
    • ResultSetHandler—interceptorChain.pluginAll(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);
      resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
      return resultSetHandler;
    }
               
    b、interceptorChain.pluginAll()調用的就是實作了Interceptor接口的plugin()方法,plugin()方法又通過Plugin.wrap(target,this)為目标對象建立一個Plugin的代理對象,添加到攔截鍊interceptorChain中。具體代碼如下:
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          // 調用實作Interceptor接口的plugin方法
          target = interceptor.plugin(target);
        }
        return target;
      }
               
    @Override
    public Object plugin(Object target) {
      	// 調用Plugin的wrap()方法,建立代理對象
        return Plugin.wrap(target,this);
    }
               
    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;
    }
               
    c、Plugin實作了 InvocationHandler接口,是以它的invoke方法會攔截所有的方法調用。invoke()方法會 對所攔截的方法進行檢測,以決定是否執行插件邏輯。
    @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          // 根據元件對象Class從signatureMap中擷取到需要攔截的方法set集合
          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);
        }
      }
               
    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
      // 擷取到所有攔截器的類
      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注解數組
      Signature[] sigs = interceptsAnnotation.value();
      // signatureMap存放所有攔截到方法,key為四大元件的Class,value為元件對應的方法set集合
      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;
    }
               
  5. PageHelper插件

    PageHelper是MyBaits架構使用最廣泛的第三方實體分頁插件,分頁助手PageHelper是将分頁的複雜操作進行封裝,使用簡單的方式即可獲得分頁的相關資料。

    使用步驟:

    • 添加依賴
      <dependency>
          <groupId>com.github.pagehelper</groupId>
          <artifactId>pagehelper</artifactId>
          <version>5.1.8</version>
      </dependency>
      <dependency>
          <groupId>com.github.jsqlparser</groupId>
          <artifactId>jsqlparser</artifactId>
          <version>1.2</version>
      </dependency>
                 
    • 配置插件
      <plugin interceptor="com.github.pagehelper.PageInterceptor">
      </plugin>
                 
    • 使用分頁
      @org.junit.Test
      public void testPagehealper() {
        PageHelper.startPage(1, 2);
        List<User> users = userDao.findAll();
        if (users != null && users.size() > 0) {
          for (User user : users) {
            System.out.println(user.toString());
          }
          PageInfo<User> pageInfo = new PageInfo<>(users);
          System.out.println("總條數:" + pageInfo.getTotal());
          System.out.println("總頁數:" + pageInfo.getPages());
          System.out.println("目前頁:" + pageInfo.getPageNum());
          System.out.println("每頁顯萬長度:" + pageInfo.getPageSize());
          System.out.println("是否第一頁:" + pageInfo.isIsFirstPage());
          System.out.println("是否最後一頁:" + pageInfo.isIsLastPage());
        }
      }