1. 程式人生 > >MyBatis攔截器自定義分頁外掛實現

MyBatis攔截器自定義分頁外掛實現

MyBaits是一個開源的優秀的持久層框架,SQL語句與程式碼分離,面向配置的程式設計,良好支援複雜資料對映,動態SQL;MyBatis 是支援定製化 SQL、儲存過程以及高階對映的優秀的持久層框架。MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。MyBatis 可以對配置和原生Map使用簡單的 XML 或註解,將介面和 Java 的 POJOs(Plain Old Java Objects,普通的 Java物件)對映成資料庫中的記錄。
通常使用MyBatis時使用以下幾種形式進行分頁:

  1. 邏輯分頁:RowBounds
  2. 物理分頁: 在SQL裡面使用LIMIT或者使用第三方外掛(PageHelper等)

    環境及介紹

    匯入核心依賴:
 <!-- SpringBoot MyBatis starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.2.2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

搭建環境步驟可以參考:SpringBoot企業中常用starter
本次主要實現一個介面org.apache.ibatis.plugin.Interceptor,在介面中有3個方法為:

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);
  1. intercept 方法是主要攔截執行方法。
  2. plugin 方法是決定當前物件是否需要生成代理物件。
  3. setProperties 設定執行時mybatis核心配置引數方法。

    Mybatis的攔截器實現機制,使用的是JDKInvocationHandler,當我們呼叫ParameterHandler,ResultSetHandler,StatementHandler,Executor的物件的時候,實際上使用的是Plugin這個代理類的物件,這個類實現了InvocationHandler介面。
    當我們呼叫ParameterHandler,ResultSetHandler,StatementHandler,Executor的物件的時候,
    實際上使用的是Plugin這個代理類的物件,這個類實現了InvocationHandler介面。

    自定義分頁實現

    建立Pager實體物件,用來進行分頁,實體內容如下:
@Data
@ToString
public class Pager {
    /*當前頁*/
    private int page;
    /*每頁大小*/
    private int size;
    /*總記錄*/
    private long total;
    /*總頁數*/
    private int totalPage;
    /*自定義分頁sql*/
    private String customSQL;
    /*分頁執行時長*/
    private long executeTime;
}

這裡customSQL變數為自定義分頁SQL,很多時候以為sql過於複雜關聯了N張表,獲取了N個欄位會造成查詢時間過於緩慢,加入這個欄位主要是為了在一些複雜的SQL中不暴力使用預設的分頁數量統計,可以自己根據SQL去除不需要的欄位,已經不需要的表連線後的SQL來執行分頁數量的統計。
定義分頁處理介面PagerHandler,內容如下:

public interface PagerHandler {
    /**
     * 獲取sql執行引數
     * @param boundSql
     * @return
     */
    public Pager getPager(BoundSql boundSql);

    /**
     * 執行分頁
     *
     * @param pager
     * @param boundSql
     * @param connection
     * @param metaObject
     * @return
     * @throws SQLException
     */
    public Pager executer(Pager pager, BoundSql boundSql, Connection connection, MetaObject metaObject) throws SQLException;
}

建立MyBats攔截器實現分頁PagerMyBatisInterceptor,該類實現介面org.apache.ibatis.plugin.Interceptor和我們自己定義的PagerHandler,重寫介面中方法。
我們還需要告訴MyBatis具體在什麼地點進行攔截,使用@Intercepts來標註:

@Intercepts(value = {
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})

主要攔截StatementHandlerprepare編譯引數方法該方法需要傳入引數型別為Connection.class, Integer.class,在MyBatis 3.4.1版本下引數只有一個Connection
在類中定義一些常量:

private final Logger log = LoggerFactory.getLogger(PagerMyBatisInterceptor.class);

private final int CONNECTION_INDEX = 0; //連線引數索引

private final DefaultReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();//預設反射工廠

private final String DELEGATE_MAPPED_STATEMENT = "delegate.mappedStatement";//反射值獲取路徑

private final String DELEGATE_PARAMETER_HANDLER = "delegate.parameterHandler";//反射值獲取路徑

具體攔截器內容:

@Component
@Intercepts(value = {
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PagerMyBatisInterceptor implements PagerHandler, Interceptor {

    private final Logger log = LoggerFactory.getLogger(PagerMyBatisInterceptor.class);

    private final int CONNECTION_INDEX = 0;

    private final DefaultReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();

    private final String DELEGATE_MAPPED_STATEMENT = "delegate.mappedStatement";

    private final String DELEGATE_PARAMETER_HANDLER = "delegate.parameterHandler";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        //資料庫連線
        Connection connection = (Connection) args[CONNECTION_INDEX];
        //負責處理Mybatis與JDBC之間Statement的互動
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        //MetaObject是Mybatis提供的一個用於方便、優雅訪問物件屬性的物件,通過它可以簡化程式碼、不需要try/catch各種reflect異常,同時它支援對JavaBean、Collection、Map三種類型物件的操作。
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
        //MappedStatement表示的是XML中的一個SQL
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(DELEGATE_MAPPED_STATEMENT);
        //SqlCommandType代表SQL型別
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        //BoundSql則是其儲存Sql語句的物件
        BoundSql boundSql = statementHandler.getBoundSql();
        //分頁物件
        Pager pager = getPager(boundSql);
        if (sqlCommandType.compareTo(SqlCommandType.SELECT) == 0 && pager != null) {
            executer(pager, boundSql, connection, metaObject);
            //執行查詢
            int left = (pager.getPage() - 1) * pager.getSize();
            int right = pager.getSize();
            String rewriteSql = boundSql.getSql() + " LIMIT " + left + "," + right;
            metaObject.setValue("boundSql.sql", rewriteSql);
        }
        long startTime = System.currentTimeMillis();
        Object proceed = invocation.proceed();
        long endTime = System.currentTimeMillis();
        log.info("SQL TYPE [{}] , SQL EXECUTE TIME [{}] SQL:\n{}", sqlCommandType, startTime - endTime, boundSql.getSql().toUpperCase());
        return proceed;
    }

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

    @Override
    public void setProperties(Properties properties) {
        //TODO 設定mybatis引數
    }

    @Override
    public Pager getPager(BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        if (parameterObject instanceof Pager) {
            return (Pager) parameterObject;
        } else if (parameterObject instanceof Map) {
            Map<String, Object> paramMap = (Map<String, Object>) parameterObject;
            Iterator<String> keys = paramMap.keySet().iterator();
            while (keys.hasNext()) {
                String key = keys.next();
                Object obj = paramMap.get(key);
                if (obj instanceof Pager) {
                    return (Pager) obj;
                }
            }

        }
        return null;
    }

    @Override
    public Pager executer(Pager pager, BoundSql boundSql, Connection connection, MetaObject metaObject) throws SQLException {
        if (pager.getPage() == 0) {
            pager.setPage(0);
        }
        if (pager.getSize() == 0) {
            pager.setSize(0);
        }
        if (pager.getCustomSQL() == null) {
            //如果自己沒有定義分頁SQL,那麼使用預設暴力分頁
            pager.setCustomSQL("SELECT COUNT(1) FROM (" + boundSql.getSql() + " ) tmp_table");
        }
        // 預編譯
        PreparedStatement prepareStatement = connection.prepareStatement(pager.getCustomSQL());
        // 預編譯執行
        ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue(DELEGATE_PARAMETER_HANDLER);
        parameterHandler.setParameters(prepareStatement); // 給sql語句設定引數
        long startTime = System.currentTimeMillis();
        ResultSet resultSet = prepareStatement.executeQuery();
        long endTime = System.currentTimeMillis();
        log.info("sql execute time {} sql:\n{}", startTime - endTime, pager.getCustomSQL().toUpperCase());
        if (resultSet.next()) {
            long total = (long) resultSet.getObject(1);// 總記錄數量
            int totalPageNum = (int) ((total + pager.getSize() - 1) / pager.getSize());
            pager.setTotal(total);
            pager.setTotalPage(totalPageNum);
            pager.setExecuteTime(startTime - endTime);
        }
        return pager;
    }
}

通過方法getPager獲取到pager物件,如果是Map引數那麼就優先第一個通過引用的傳遞在BoundSql物件中獲取到,然後執行分頁獲取裡面的值進行計算,通過引用物件返回總記錄數,總頁數等。
在SpringBoot中如果需要使攔截器生效只需要在型別使用@Component將該類交給Spring IOC管理即可,至於攔截器順序如:
有攔截器 PagerMyBatisInterceptorOneInterceptor ,想要分頁攔截器作為第二個攔截器只需要在類上標註@ConditionalOnBean(OneInterceptor)即可,在第一個攔截器例項化後再例項化第二個攔截器.

Pager使用方式

 List<Map<String, Object>> selectUser(Pager pager);
 List<Map<String, Object>> selectUser(Map<String,Object> paramMap);

建立一個Pager物件傳入即可。
本文原始碼地址:https://github.com/450255266/open-doubi/tree/master/spring-boot/custom-mybatis-pa