天天看點

MyBatis 插件講解

關于Mybatis插件,大部分人都知道,也都使用過,但很多時候,我們僅僅是停留在表面上,知道Mybatis插件可以在DAO層進行攔截,如列印執行的SQL語句日志,做一些權限控制,分頁等功能;

但對其内部實作機制,涉及的軟體設計模式,程式設計思想往往沒有深入的了解。

本篇案例将幫助讀者對Mybatis插件的使用場景,實作機制,以及其中涉及的程式設計思想進行一個小結,希望對以後的程式設計開發工作有所幫助。

Mybatis插件适用場景:

  • 分頁功能

    mybatis的分頁預設是基于記憶體分頁的(查出所有,再截取),資料量大的情況下效率較低,不過使用mybatis插件可以改變該行為,隻需要攔截StatementHandler類的prepare方法,改變要執行的SQL語句為分頁語句即可;

  • 公共字段統一指派

    一般業務系統都會有建立者,建立時間,修改者,修改時間四個字段,對于這四個字段的指派,實際上可以在DAO層統一攔截處理,可以用mybatis插件攔截Executor類的update方法,對相關參數進行統一指派即可;

  • 性能監控

    對于SQL語句執行的性能監控,可以通過攔截Executor類的update, query等方法,用日志記錄每個方法執行的時間;

  • 其它

    其實mybatis擴充性還是很強的,基于插件機制,基本上可以控制SQL執行的各個階段,如執行階段,參數處理階段,文法建構階段,結果集處理階段,具體可以根據項目業務來實作對應業務邏輯。

Mybatis插件實際上就是一個攔截器,應用代理模式,在方法級别上進行攔截。

1.MyBatis 插件接口

MyBatis 架構在設計的時候,就已經為插件的開發預留了相關接口,如下:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}           

複制

這個接口中就三個方法,第一個方法必須實作,後面兩個方法都是可選的。三個方法作用分别如下:

  1. intercept:這個就是具體的攔截方法,我們自定義 MyBatis 插件時,一般都需要重寫該方法,我們插件所完成的工作也都是在該方法中完成的。
  2. plugin:這個方法的參數 target 就是攔截器要攔截的對象,一般來說我們不需要重寫該方法。Plugin.wrap 方法會自動判斷攔截器的簽名和被攔截對象的接口是否比對,如果比對,才會通過動态代理攔截目标對象。
  3. setProperties:這個方法用來傳遞插件的參數,可以通過參數來改變插件的行為。我們定義好插件之後,需要對插件進行配置,在配置的時候,可以給插件設定相關屬性,設定的屬性可以通過該方法擷取到。插件屬性設定像下面這樣:
<plugins>
    <plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
        <property name="xxx" value="xxx"/>
    </plugin>
</plugins>           

複制

2.MyBatis 攔截器簽名

攔截器定義好了後,攔截誰?

這個就需要攔截器簽名來完成了!

攔截器簽名是一個名為 @Intercepts 的注解,該注解中可以通過 @Signature 配置多個簽名。@Signature 注解中則包含三個屬性:

  • type: 攔截器需要攔截的接口,有 4 個可選項,分别是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
  • method: 攔截器所攔截接口中的方法名,也就是前面四個接口中的方法名,接口和方法要對應上。
  • args: 攔截器所攔截方法的參數類型,通過方法名和參數類型可以鎖定唯一一個方法。

一個簡單的簽名可能像下面這樣:

@Intercepts(@Signature(
        type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class}
))
public class CamelInterceptor implements Interceptor {
    //...
}           

複制

3.被攔截的對象

根據前面的介紹,被攔截的對象主要有如下四個:

Executor

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}           

複制

各方法含義分别如下:

  • update:該方法會在所有的 INSERT、 UPDATE、 DELETE 執行時被調用,如果想要攔截這些操作,可以通過該方法實作。
  • query:該方法會在 SELECT 查詢方法執行時被調用,方法參數攜帶了很多有用的資訊,如果需要擷取,可以通過該方法實作。
  • queryCursor:當 SELECT 的傳回類型是 Cursor 時,該方法會被調用。
  • flushStatements:當 SqlSession 方法調用 flushStatements 方法或執行的接口方法中帶有 @Flush 注解時該方法會被觸發。
  • commit:當 SqlSession 方法調用 commit 方法時該方法會被觸發。
  • rollback:當 SqlSession 方法調用 rollback 方法時該方法會被觸發。
  • getTransaction:當 SqlSession 方法擷取資料庫連接配接時該方法會被觸發。
  • close:該方法在懶加載擷取新的 Executor 後會被觸發。
  • isClosed:該方法在懶加載執行查詢前會被觸發。

ParameterHandler

public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps) throws SQLException;

}           

複制

各方法含義分别如下:

  • getParameterObject:在執行存儲過程處理出參的時候該方法會被觸發。
  • setParameters:設定 SQL 參數時該方法會被觸發。

ResultSetHandler

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}           

複制

各方法含義分别如下:

  • handleResultSets:該方法會在所有的查詢方法中被觸發(除去傳回值類型為 Cursor<E> 的查詢方法),一般來說,如果我們想對查詢結果進行二次處理,可以通過攔截該方法實作。
  • handleCursorResultSets:當查詢方法的傳回值類型為 Cursor<E> 時,該方法會被觸發。
  • handleOutputParameters:使用存儲過程處理出參的時候該方法會被調用。

StatementHandler

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}           

複制

各方法含義分别如下:

  • prepare:該方法在資料庫執行前被觸發。
  • parameterize:該方法在 prepare 方法之後執行,用來處理參數資訊。
  • batch:如果 MyBatis 的全劇配置中配置了

    defaultExecutorType=”BATCH”

    ,執行資料操作時該方法會被調用。
  • update:更新操作時該方法會被觸發。
  • query:該方法在 SELECT 方法執行時會被觸發。
  • queryCursor:該方法在 SELECT 方法執行時,并且傳回值為 Cursor 時會被觸發。

在開發一個具體的插件時,我們應當根據自己的需求來決定到底攔截哪個方法。

4.開發分頁插件

4.1 記憶體分頁

MyBatis 中提供了一個不太好用的記憶體分頁功能,就是一次性把所有資料都查詢出來,然後在記憶體中進行分頁處理,這種分頁方式效率很低,基本上沒啥用,但是如果我們想要自定義分頁插件,就需要對這種分頁方式有一個簡單了解。

記憶體分頁的使用方式如下,首先在 Mapper 中添加 RowBounds 參數,如下:

public interface UserMapper {
    List<User> getAllUsersByPage(RowBounds rowBounds);
}           

複制

然後在 XML 檔案中定義相關 SQL:

<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
    select * from user
</select>           

複制

可以看到,在 SQL 定義時,壓根不用管分頁的事情,MyBatis 會查詢到所有的資料,然後在記憶體中進行分頁處理。

Mapper 中方法的調用方式如下:

@Test
public void test3() {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    RowBounds rowBounds = new RowBounds(1,2);
    List<User> list = userMapper.getAllUsersByPage(rowBounds);
    for (User user : list) {
        System.out.println("user = " + user);
    }
}           

複制

建構 RowBounds 時傳入兩個參數,分别是 offset 和 limit,對應分頁 SQL 中的兩個參數。也可以通過 RowBounds.DEFAULT 的方式建構一個 RowBounds 執行個體,這種方式建構出來的 RowBounds 執行個體,offset 為 0,limit 則為 Integer.MAX_VALUE,也就相當于不分頁。

這就是 MyBatis 中提供的一個很不實用的記憶體分頁功能。

了解了 MyBatis 自帶的記憶體分頁之後,接下來我們就可以來看看如何自定義分頁插件了。

4.2 自定義分頁插件

參考:

https://segmentfault.com/a/1190000039305062?utm_source=tag-newest

https://www.cnblogs.com/chenpi/p/10498921.html

https://www.jianshu.com/p/cdff18f8cb97

https://segmentfault.com/a/1190000021220956