MyBatis 示例-插件

簡介

利用 MyBatis Plugin 插件技術實現分頁功能。

分頁插件實現思路如下:

  • 業務代碼在 ThreadLocal 中保存分頁信息;
  • MyBatis Interceptor 攔截查詢請求,獲取分頁信息,實現分頁操作,封裝分頁列表數據返回;

測試類:com.yjw.demo.PageTest

插件開發過程

確定需要攔截的簽名

MyBatis 插件可以攔截四大對象中的任意一個,從 Plugin 源碼中可以看到它需要註冊簽名才能夠運行插件,簽名需要確定一些要素。

確定需要攔截的對象

  • Executor 是執行 SQL 的全過程,包括組裝參數,組裝結果集返回和執行 SQL 過程,都可以攔截。
  • StatementHandler 是執行 SQL 的過程,我們可以重寫執行 SQL 的過程。
  • ParameterHandler 是攔截執行 SQL 的參數組裝,我們可以重寫組裝參數規則。
  • ResultSetHandler 用於攔截執行結果的組裝,我們可以重寫組裝結果的規則。

攔截方法和參數

當確定了需要攔截什麼對象,接下來就要確定需要攔截什麼方法和方法的參數。比如分頁插件需要攔截 Executor 的 query 方法,我們先看看 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);
 
}

以上的任何方法都可以攔截,從接口定義而言,query 方法有兩個,我們可以按照代碼清單來定義簽名。

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

其中,@Intercepts 說明它是一個攔截器。@Signature 是註冊攔截器簽名的地方,type 是四大對象中的一個,method 是需要攔截的方法,args 是方法的參數。

插件接口定義

在 MyBatis 中開發插件,需要實現 Interceptor 接口,接口的定義如下:

public interface Interceptor {
 
  Object intercept(Invocation invocation) throws Throwable;
 
  Object plugin(Object target);
 
  void setProperties(Properties properties);
 
}
  • intercept 方法:它將直接覆蓋你所攔截對象原有的方法,因此它是插件的核心方法。通過 invocation 參數可以反射調度原來對象的方法。
  • plugin 方法:target 是被攔截對象,它的作用是給被攔截對象生成一個代理對象,並返回它。爲了方便 MyBatis 使用 org.apache.ibatis.plugin.Plugin 中的 wrap 靜態方法提供生成代理對象。
  • setProperties 方法:允許在 plugin 元素中配置所需參數,方法在插件初始化的時候就被調用了一次,然後把插件對象存入到配置中,以便後面再取出。

實現類

根據分頁插件的實現思路,定義了三個類。

Page 類

Page 類繼承了 ArrayList 類,用來封裝分頁信息和列表數據。

/**
 * 分頁返回對象
 * 
 * @author yinjianwei
 * @date 2018/11/05
 */
public class Page<E> extends ArrayList<E> {

    private static final long serialVersionUID = 1L;

    /**
     * 頁碼,從1開始
     */
    private int pageNum;
    /**
     * 頁面大小
     */
    private int pageSize;
    /**
     * 起始行
     */
    private int startRow;
    /**
     * 末行
     */
    private int endRow;
    /**
     * 總數
     */
    private long total;
    /**
     * 總頁數
     */
    private int pages;

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getStartRow() {
        return startRow;
    }

    public void setStartRow(int startRow) {
        this.startRow = startRow;
    }

    public int getEndRow() {
        return endRow;
    }

    public void setEndRow(int endRow) {
        this.endRow = endRow;
    }

    public long getTotal() {
        return total;
    }

    public void setTotal(long total) {
        this.total = total;
        this.pages = (int)(total / pageSize + (total % pageSize == 0 ? 0 : 1));
        if (pageNum > pages) {
            pageNum = pages;
        }
        this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
        this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    /**
     * 返回當前對象
     * 
     * @return
     */
    public List<E> getResult() {
        return this;
    }

}

PageHelper 類

PageHelper 類是分頁的幫助類,主要利用 ThreadLocal 線程變量存儲分頁信息。代碼清單如下:

/**
 * 分頁幫助類
 * 
 * @author yinjianwei
 * @date 2018/11/05
 */
@SuppressWarnings("rawtypes")
public class PageHelper {

    private static final ThreadLocal<Page> PAGE_THREADLOCAT = new ThreadLocal<Page>();

    /**
     * 設置線程局部變量分頁信息
     * 
     * @param page
     */
    public static void setPageThreadLocal(Page page) {
        PAGE_THREADLOCAT.set(page);
    }

    /**
     * 獲取線程局部變量分頁信息
     * 
     * @return
     */
    public static Page getPageThreadLocal() {
        return PAGE_THREADLOCAT.get();
    }

    /**
     * 清空線程局部變量分頁信息
     */
    public static void pageThreadLocalClear() {
        PAGE_THREADLOCAT.remove();
    }

    /**
     * 設置分頁參數
     * 
     * @param pageNum
     * @param pageSize
     */
    public static void startPage(Integer pageNum, Integer pageSize) {
        Page page = new Page();
        page.setPageNum(pageNum);
        page.setPageSize(pageSize);
        setPageThreadLocal(page);
    }

}

PageInterceptor 類

PageInterceptor 類實現了 Interceptor 接口,是分頁插件的核心類。代碼清單如下:

/**
 * 分頁攔截器
 * 
 * @author yinjianwei
 * @date 2018/11/05
 */
@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 {

    private Field additionalParametersField;

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Executor executor = (Executor)invocation.getTarget();
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement)args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds)args[2];
        ResultHandler resultHandler = (ResultHandler)args[3];
        CacheKey cacheKey;
        BoundSql boundSql;
        // 4個參數
        if (args.length == 4) {
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        }
        // 6個參數
        else {
            cacheKey = (CacheKey)args[4];
            boundSql = (BoundSql)args[5];
        }
        // 判斷是否需要分頁
        Page page = PageHelper.getPageThreadLocal();
        // 不執行分頁
        if (page.getPageNum() <= 0) {
            return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        // count查詢
        MappedStatement countMs = newCountMappedStatement(ms);
        String sql = boundSql.getSql();
        String countSql = "select count(1) from (" + sql + ") _count";
        BoundSql countBoundSql =
            new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
        Map<String, Object> additionalParameters = (Map<String, Object>)additionalParametersField.get(boundSql);
        for (Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {
            countBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue());
        }
        CacheKey countCacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countBoundSql);
        Object countResult =
            executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countCacheKey, countBoundSql);
        Long count = (Long)((List)countResult).get(0);
        page.setTotal(count);
        // 分頁查詢
        String pageSql = sql + " limit " + page.getStartRow() + "," + page.getPageSize();
        BoundSql pageBoundSql =
            new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
        for (Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {
            pageBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue());
        }
        CacheKey pageCacheKey = executor.createCacheKey(ms, parameter, rowBounds, pageBoundSql);
        List listResult = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageCacheKey, pageBoundSql);
        page.addAll(listResult);
        // 清空線程局部變量分頁信息
        PageHelper.pageThreadLocalClear();
        return page;
    }

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

    @Override
    public void setProperties(Properties properties) {
        try {
            additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException | SecurityException e) {
            e.printStackTrace();
        }
    }

    /**
     * 創建count的MappedStatement
     * 
     * @param ms
     * @return
     */
    private MappedStatement newCountMappedStatement(MappedStatement ms) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_count",
            ms.getSqlSource(), ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        // count查詢返回值int
        List<ResultMap> resultMaps = new ArrayList<ResultMap>();
        ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId() + "_count", Long.class,
            new ArrayList<ResultMapping>(0)).build();
        resultMaps.add(resultMap);
        builder.resultMaps(resultMaps);
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }

}

配置

MyBatis 配置文件增加 plugin 配置項。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

    <typeHandlers>
        <typeHandler javaType="com.yjw.demo.mybatis.common.constant.Sex"
                     jdbcType="TINYINT"
                     handler="com.yjw.demo.mybatis.common.type.SexEnumTypeHandler"/>
    </typeHandlers>

    <plugins>
        <plugin interceptor="com.yjw.demo.mybatis.common.page.PageInterceptor">
        </plugin>
    </plugins>
</configuration>

 

MyBatis 實用篇

MyBatis 概念

MyBatis 示例-簡介

MyBatis 示例-類型處理器

MyBatis 示例-傳遞多個參數

MyBatis 示例-主鍵回填

MyBatis 示例-動態 SQL

MyBatis 示例-聯合查詢

MyBatis 示例-緩存

MyBatis 示例-插件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章