MyBatis原理系列(七)-手把手帶你了解如何自定義插件

MyBatis原理系列(一)-手把手帶你閱讀MyBatis源碼
MyBatis原理系列(二)-手把手帶你了解MyBatis的啟動流程
MyBatis原理系列(三)-手把手帶你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的關(guān)系
MyBatis原理系列(四)-手把手帶你了解MyBatis的Executor執(zhí)行器
MyBatis原理系列(五)-手把手帶你了解Statement、StatementHandler、MappedStatement間的關(guān)系
MyBatis原理系列(六)-手把手帶你了解BoundSql的創(chuàng)建過程
MyBatis原理系列(七)-手把手帶你了解如何自定義插件
MyBatis原理系列(八)-手把手帶你了解一級緩存和二級緩存
MyBatis原理系列(九)-手把手帶你了解MyBatis事務(wù)管理機制

MyBatis的一個重要的特點就是插件機制,使得MyBatis的具備較強的擴展性,我們可以根據(jù)MyBatis的插件機制實現(xiàn)自己的個性化業(yè)務(wù)需求。

1. 初識插件

我們在執(zhí)行查詢的時候,如果sql沒有加上分頁條件,數(shù)據(jù)量過大的話會造成內(nèi)存溢出,因此我們可以通過MyBatis提供的插件機制來攔截sql,并進行sql改寫。MyBatis的插件是通過動態(tài)代理來實現(xiàn)的,并且會形成一個插件鏈。原理類似于攔截器,攔截我們需要處理的對象,進行自定義邏輯后,返回一個代理對象,進行下一個攔截器的處理。

我們先來看下一個簡單插件的模板,首先要實現(xiàn)一個Interceptor接口,并實現(xiàn)三個方法。并加上@Intercepts注解。接下來我們以分頁插件為例將對每個細節(jié)進行講解。

/**
 * @ClassName : PagePlugin
 * @Description : 分頁插件
 * @Date: 2020/12/29
 */
@Intercepts({})
public class PagePlugin implements Interceptor {
    
    private Properties properties;
    
    @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) {
        this.properties = properties;
    }
}

2. 攔截對象

在進行插件創(chuàng)建的時候,需要指定攔截對象。@Intercepts注解指定需要攔截的方法簽名,內(nèi)容是個Signature類型的數(shù)組,而Signature就是對攔截對象的描述。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  /**
   * Returns method signatures to intercept.
   *
   * @return method signatures
   */
  Signature[] value();
}

Signature 需要指定攔截對象中方法的信息的描述。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  /**
   * 對象類型
   */
  Class<?> type();

  /**
   * 方法名
   */
  String method();

  /**
   * 參數(shù)類型
   */
  Class<?>[] args();
}

在MyBatis中,我們只能對以下四種類型的對象進行攔截

  • ParameterHandler : 對sql參數(shù)進行處理
  • ResultSetHandler : 對結(jié)果集對象進行處理
  • StatementHandler : 對sql語句進行處理
  • Executor : 執(zhí)行器,執(zhí)行增刪改查

現(xiàn)在我們需要對sql進行改寫,因此可以需要攔截Executor的query方法進行攔截

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

3. 攔截實現(xiàn)

每個插件除了指定攔截的方法后,還需要實現(xiàn)Interceptor接口。Interceptor接口有以下三個方法。其中intercept是我們必須要實現(xiàn)的方法,在這里面我們需要實現(xiàn)自定義邏輯。其它兩個方法給出了默認實現(xiàn)。

public interface Interceptor {

  /**
   * 進行攔截處理
   * @param invocation
   * @return
   * @throws Throwable
   */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   * 返回代理對象
   * @param target
   * @return
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  /**
   * 設(shè)置配置屬性
   * @param properties
   */
  default void setProperties(Properties properties) {
    // NOP
  }

}

因此我們實現(xiàn)intercept方法即可,因為我們要改寫查詢sql語句,因此需要攔截Executor的query方法,然后修改RowBounds參數(shù)中的limit,如果limit大于1000,我們強制設(shè)置為1000。

@Slf4j
@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
public class PagePlugin implements Interceptor {

    private Properties properties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds)args[2];
        log.info("執(zhí)行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
        if(rowBounds != null){
            if(rowBounds.getLimit() > 1000){
                Field field = rowBounds.getClass().getDeclaredField("limit");
                field.setAccessible(true);
                field.set(rowBounds, 1000);
            }
        }else{
            rowBounds = new RowBounds(0 ,100);
            args[2] = rowBounds;
        }
        log.info("執(zhí)行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

4. 加載流程

以上我們已經(jīng)實現(xiàn)了一個簡單的插件,在執(zhí)行查詢的時候?qū)uery方法進行攔截,并且修改分頁參數(shù)。但是我們現(xiàn)在還沒有進行插件配置,只有配置了插件,MyBatis才能啟動過程中加載插件。

4.1 xml配置插件

在mybatis-config.xml中添加plugins標簽,并且配置我們上面實現(xiàn)的plugin.

   <plugins>
        <plugin interceptor="com.example.demo.mybatis.PagePlugin">
        </plugin>
    </plugins>
4.2 XMLConfigBuilder加載插件

在啟動流程中加載插件那篇文章中介紹到SqlSessionFactoryBuilder的build方法,其中XMLConfigBuilder這個解析器中的parse()方法就會讀取plugins標簽下的插件,并加載Configuration中的InterceptorChain中。

// SqlSessionFactoryBuilder
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            var5 = this.build(parser.parse());
        } catch (Exception var14) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
        } finally {
            ErrorContext.instance().reset();

            try {
                inputStream.close();
            } catch (IOException var13) {
            }

        }

        return var5;
    }

可見XMLConfigBuilder這個parse()方法就是解析xml中配置的各個標簽。

// XMLConfigBuilder
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      // 解析properties節(jié)點
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      // 記載插件
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

XMLConfigBuilder 的pluginElement就是遍歷plugins下的plugin加載到interceptorChain中。

  // XMLConfigBuilder
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      // 遍歷每個plugin插件
      for (XNode child : parent.getChildren()) {
        // 讀取插件的實現(xiàn)類
        String interceptor = child.getStringAttribute("interceptor");
        // 讀取插件配置信息
        Properties properties = child.getChildrenAsProperties();
        // 創(chuàng)建interceptor對象
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        // 加載到interceptorChain鏈中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

InterceptorChain 是一個interceptor集合,相當于是一層層包裝,后一個插件就是對前一個插件的包裝,并返回一個代理對象。

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

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}
4.3 創(chuàng)建插件對象

因為我們需要對攔截對象進行攔截,并進行一層包裝返回一個代理類,那是什么時候進行處理的呢?以Executor為例,在創(chuàng)建Executor對象的時候,會有以下代碼。

  // Configuration
  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);
    }
    // 創(chuàng)建插件對象
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

創(chuàng)建完Executor對象后,就會調(diào)用interceptorChain.pluginAll()方法,實際調(diào)用的是每個Interceptor的plugin()方法。plugin()就是對目標對象的一個代理,并且生成一個代理對象返回。而Plugin.wrap()就是進行包裝的操作。

  // Interceptor
  /**
   * 返回代理對象
   * @param target
   * @return
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

Plugin的wrap()主要進行了以下步驟:

  • 獲取攔截器攔截的方法,以攔截對象為key,攔截方法集合為value
  • 獲取目標對象的class對,比如Executor對象
  • 如果攔截器中攔截的對象包含目標對象實現(xiàn)的接口,則返回攔截的接口
  • 創(chuàng)建代理類Plugin對象,Plugin實現(xiàn)了InvocationHandler接口,最終對目標對象的調(diào)用都會調(diào)用Plugin的invocate方法。
  // Plugin
  public static Object wrap(Object target, Interceptor interceptor) {
    // 獲取攔截器攔截的方法,以攔截對象為key,攔截方法為value
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // 獲取目標對象的class對象
    Class<?> type = target.getClass();
    // 如果攔截器中攔截的對象包含目標對象實現(xiàn)的接口,則返回攔截的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 如果對目標對象進行了攔截
    if (interfaces.length > 0) {
      // 創(chuàng)建代理類Plugin對象
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

5. 例子

我們已經(jīng)了解MyBatis插件的配置,創(chuàng)建,實現(xiàn)流程,接下來就以一開始我們提出的例子來介紹實現(xiàn)一個插件應(yīng)該做哪些。

5.1 確定攔截對象

因為我們要對查詢sql分頁參數(shù)進行改寫,因此可以攔截Executor的query方法,并進行分頁參數(shù)的改寫

@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
5.2 實現(xiàn)攔截接口

實現(xiàn)Interceptor接口,并且實現(xiàn)intercept實現(xiàn)我們的攔截邏輯

@Slf4j
@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
public class PagePlugin implements Interceptor {

    private Properties properties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds)args[2];
        log.info("執(zhí)行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
        if(rowBounds != null){
            if(rowBounds.getLimit() > 1000){
                Field field = rowBounds.getClass().getDeclaredField("limit");
                field.setAccessible(true);
                field.set(rowBounds, 1000);
            }
        }else{
            rowBounds = new RowBounds(0 ,100);
            args[2] = rowBounds;
        }
        log.info("執(zhí)行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

5.3 配置插件

在mybatis-config.xml中配置以下插件

    <plugins>
        <plugin interceptor="com.example.demo.mybatis.PagePlugin">
        </plugin>
    </plugins>
5.4 測試

com/example/demo/dao/TTestUserMapper.java 新增selectByPage方法

    List<TTestUser> selectByPage(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);

mapper/TTestUserMapper.xml 新增對應(yīng)的sql


  <select id="selectByPage" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from t_test_user
    <if test="offset != null">
      limit #{offset}, #{pageSize}
    </if>
  </select>

最終測試代碼,我們沒有在查詢的時候指定分頁參數(shù)。

public static void main(String[] args) {
        try {
            // 1. 讀取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 創(chuàng)建SqlSessionFactory工廠
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 獲取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. 獲取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 執(zhí)行接口方法
            List<TTestUser> list2 = userMapper.selectByPage(null, null);
            System.out.println("list2="+list2.size());
            // 6. 提交事物
            sqlSession.commit();
            // 7. 關(guān)閉資源
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

最終打印的日志如下,我們可以看到rowBounds已經(jīng)被我們強制修改了只能查處1000條數(shù)據(jù)。

10:11:49.313 [main] INFO com.example.demo.mybatis.PagePlugin - 執(zhí)行前, rowBounds = [{"offset":0,"limit":2147483647}]
10:11:58.015 [main] INFO com.example.demo.mybatis.PagePlugin - 執(zhí)行后, rowBounds = [{"offset":0,"limit":1000}]
10:12:03.211 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
10:12:04.269 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 749981943.
10:12:04.270 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2cb3d0f7]
10:12:04.283 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user 
10:12:04.335 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==> Parameters: 
list2=1000

總結(jié)

本文對MyBatis的插件進行講解,介紹了每個接口的作用,以及插件的啟動,創(chuàng)建,實現(xiàn)的步驟,希望對家能夠有所幫助

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容