MyBatis插件開發(fā):簡單分頁插件

原文地址:https://xeblog.cn/articles/20

MyBatis插件開發(fā)流程

  • 類實現Interceptor接口;
  • 類上添加注解@Intercepts({@Signature(type, method, args)})
    • type:需要攔截的對象歧蕉,只可取四大對象之一Executor.class灾部、StatementHandler.class、ParameterHandler.class惯退、ResultSetHandler.class赌髓。
    • method:攔截的對象方法。
    • args:攔截的對象方法參數催跪。
  • 實現攔截的方法Object intercept(Invocation invocation)锁蠕。

Interceptor接口

public interface Interceptor {

    /**
     * 此方法將直接覆蓋被攔截對象的原有方法
     *
     * @param invocation 通過該對象可以反射調度攔截對象的方法
     * @return
     * @throws Throwable
     */
    Object intercept(Invocation invocation) throws Throwable;

    /**
     * 為被攔截對象生成一個代理對象,并返回它
     *
     * @param target 被攔截的對象
     * @return
     */
    Object plugin(Object target);

    /**
     * 設置插件配置的參數
     *
     * @param properties 插件配置的參數
     */
    void setProperties(Properties properties);

}

簡單分頁插件開發(fā)

確定攔截的方法簽名

需要在實現Interceptor接口的類上加入@Intercepts({@Signature(type, method, args)})注解才能夠運行插件懊蒸。

type-攔截的對象

  • Executor 執(zhí)行的SQL 全過程荣倾,包括組裝參數、組裝結果返回和執(zhí)行SQL的過程等都可以攔截骑丸。
  • StatementHandler 執(zhí)行SQL的過程舌仍,攔截該對象可以重寫執(zhí)行SQL的過程。
  • ParameterHandler 執(zhí)行SQL 的參數組裝通危,攔截該對象可以重寫組裝參數的規(guī)則铸豁。
  • ResultSetHandler 執(zhí)行結果的組裝,攔截該對象可以重寫組裝結果的規(guī)則黄鳍。

對于分頁插件推姻,我們只需要攔截StatementHandler對象平匈,重寫SELECT類型的SQL語句框沟,實現分頁功能藏古。

method-攔截的方法

我們已經能夠確定攔截的對象是StatementHandler了,現在我們要確定攔截的是哪個方法忍燥,因為StatementHandler是通過prepare方法對SQL進行預編譯的拧晕,所以我們需要對prepare方法進行攔截,在這個方法執(zhí)行之前梅垄,完成SQL的重新編寫(加入limit)厂捞。

StatementHandler

public interface StatementHandler {

  /**
   * 預編譯SQL
   *
   * @param connection
   * @return
   * @throws SQLException
   */
  Statement prepare(Connection connection)
      throws SQLException;

  /**
   * 設置參數
   *
   * @param statement
   * @throws SQLException
   */
  void parameterize(Statement statement)
      throws SQLException;

  /**
   * 批處理
   *
   * @param statement
   * @throws SQLException
   */
  void batch(Statement statement)
      throws SQLException;

  /**
   * 執(zhí)行更新操作
   *
   * @param statement
   * @return 返回影響行數
   * @throws SQLException
   */
  int update(Statement statement)
      throws SQLException;

  /**
   * 執(zhí)行查詢操作,將結果交給ResultHandler進行結果的組裝
   *
   * @param statement
   * @param resultHandler
   * @param <E>
   * @return 返回查詢的數據列表
   * @throws SQLException
   */
  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  /**
   * 得到綁定的sql
   * 
   * @return
   */
  BoundSql getBoundSql();

  /**
   * 得到參數處理器
   * 
   * @return
   */
  ParameterHandler getParameterHandler();

}

args-攔截的參數

args是一個Class類型的數組队丝,表示的是被攔截方法的參數列表靡馁。由于我們已經確定了攔截的是StatementHandlerprepare方法,而該方法只有一個參數Connection机久,所以我們只需要攔截這一個參數即可臭墨。

實現攔截方法

定義一個封裝分頁參數的類Page

package cn.xeblog.pojo;

public class Page {

    /**
     * 當前頁碼
     */
    private Integer pageIndex;
    /**
     * 每頁數據條數
     */
    private Integer pageSize;
    /**
     * 總數據數
     */
    private Integer total;
    /**
     * 總頁數
     */
    private Integer totalPage;

    public Page() {
    }

    public Page(Integer pageIndex, Integer pageSize) {
        this.pageIndex = pageIndex;
        this.pageSize = pageSize;
    }
    // 省略get、set方法...
}

實現插件分頁的功能

package cn.xeblog.plugin;

import cn.xeblog.pojo.Page;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class}
)})
public class PagingPlugin implements Interceptor {

    /**
     * 默認頁碼
     */
    private Integer defaultPageIndex;
    /**
     * 默認每頁數據條數
     */
    private Integer defaultPageSize;

    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = getUnProxyObject(invocation);
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        String sql = getSql(metaObject);
        if (!checkSelect(sql)) {
            // 不是select語句膘盖,進入責任鏈下一層
            return invocation.proceed();
        }

        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        Object parameterObject = boundSql.getParameterObject();
        Page page = getPage(parameterObject);
        if (page == null) {
            // 沒有傳入page對象胧弛,不執(zhí)行分頁處理,進入責任鏈下一層
            return invocation.proceed();
        }

        // 設置分頁默認值
        if (page.getPageIndex() == null) {
            page.setPageIndex(this.defaultPageIndex);
        }
        if (page.getPageSize() == null) {
            page.setPageSize(this.defaultPageSize);
        }
        // 設置分頁總數侠畔,數據總數
        setTotalToPage(page, invocation, metaObject, boundSql);
        // 校驗分頁參數
        checkPage(page);
        return changeSql(invocation, metaObject, boundSql, page);
    }

    public Object plugin(Object target) {
        // 生成代理對象
        return Plugin.wrap(target, this);
    }

    public void setProperties(Properties properties) {
        // 初始化配置的默認頁碼结缚,無配置則默認1
        this.defaultPageIndex = Integer.parseInt(properties.getProperty("default.pageIndex", "1"));
        // 初始化配置的默認數據條數,無配置則默認20
        this.defaultPageSize = Integer.parseInt(properties.getProperty("default.pageSize", "20"));
    }

    /**
     * 從代理對象中分離出真實對象
     *
     * @param invocation
     * @return
     */
    private StatementHandler getUnProxyObject(Invocation invocation) {
        // 取出被攔截的對象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStmtHandler = SystemMetaObject.forObject(statementHandler);
        Object object = null;
        // 分離代理對象
        while (metaStmtHandler.hasGetter("h")) {
            object = metaStmtHandler.getValue("h");
            metaStmtHandler = SystemMetaObject.forObject(object);
        }

        return object == null ? statementHandler : (StatementHandler) object;
    }

    /**
     * 判斷是否是select語句
     *
     * @param sql
     * @return
     */
    private boolean checkSelect(String sql) {
        // 去除sql的前后空格软棺,并將sql轉換成小寫
        sql = sql.trim().toLowerCase();
        return sql.indexOf("select") == 0;
    }

    /**
     * 獲取分頁參數
     *
     * @param parameterObject
     * @return
     */
    private Page getPage(Object parameterObject) {
        if (parameterObject == null) {
            return null;
        }

        if (parameterObject instanceof Map) {
            // 如果傳入的參數是map類型的红竭,則遍歷map取出Page對象
            Map<String, Object> parameMap = (Map<String, Object>) parameterObject;
            Set<String> keySet = parameMap.keySet();
            for (String key : keySet) {
                Object value = parameMap.get(key);
                if (value instanceof Page) {
                    // 返回Page對象
                    return (Page) value;
                }
            }
        } else if (parameterObject instanceof Page) {
            // 如果傳入的是Page類型,則直接返回該對象
            return (Page) parameterObject;
        }

        // 初步判斷并沒有傳入Page類型的參數喘落,返回null
        return null;
    }

    /**
     * 獲取數據總數
     *
     * @param invocation
     * @param metaObject
     * @param boundSql
     * @return
     */
    private int getTotal(Invocation invocation, MetaObject metaObject, BoundSql boundSql) {
        // 獲取當前的mappedStatement對象
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // 獲取配置對象
        Configuration configuration = mappedStatement.getConfiguration();
        // 獲取當前需要執(zhí)行的sql
        String sql = getSql(metaObject);
        // 改寫sql語句德崭,實現返回數據總數 $_paging取名是為了防止數據庫表重名
        String countSql = "select count(*) as total from (" + sql + ") $_paging";
        // 獲取攔截方法參數,攔截的是connection對象
        Connection connection = (Connection) invocation.getArgs()[0];
        PreparedStatement pstmt = null;
        int total = 0;

        try {
            // 預編譯查詢數據總數的sql語句
            pstmt = connection.prepareStatement(countSql);
            // 構建boundSql對象
            BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(),
                    boundSql.getParameterObject());
            // 構建parameterHandler用于設置sql參數
            ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(),
                    countBoundSql);
            // 設置sql參數
            parameterHandler.setParameters(pstmt);
            //執(zhí)行查詢
            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                total = rs.getInt("total");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (pstmt != null) {
                try {
                    pstmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

        // 返回總數據數
        return total;
    }

    /**
     * 設置總數據數揖盘、總頁數
     *
     * @param page
     * @param invocation
     * @param metaObject
     * @param boundSql
     */
    private void setTotalToPage(Page page, Invocation invocation, MetaObject metaObject, BoundSql boundSql) {
        // 總數據數
        int total = getTotal(invocation, metaObject, boundSql);
        // 計算總頁數
        int totalPage = total / page.getPageSize();
        if (total % page.getPageSize() != 0) {
            totalPage = totalPage + 1;
        }

        page.setTotal(total);
        page.setTotalPage(totalPage);
    }

    /**
     * 校驗分頁參數
     *
     * @param page
     */
    private void checkPage(Page page) {
        // 如果當前頁碼大于總頁數眉厨,拋出異常
        if (page.getPageIndex() > page.getTotalPage()) {
            throw new RuntimeException("當前頁碼[" + page.getPageIndex() + "]大于總頁數[" + page.getTotalPage() + "]");
        }
        // 如果當前頁碼小于總頁數,拋出異常
        if (page.getPageIndex() < 1) {
            throw new RuntimeException("當前頁碼[" + page.getPageIndex() + "]小于[1]");
        }
    }

    /**
     * 修改當前查詢的sql
     *
     * @param invocation
     * @param metaObject
     * @param boundSql
     * @param page
     * @return
     */
    private Object changeSql(Invocation invocation, MetaObject metaObject, BoundSql boundSql, Page page) throws Exception {
        // 獲取當前查詢的sql
        String sql = getSql(metaObject);
        // 修改sql兽狭,$_paging_table_limit取名是為了防止數據庫表重名
        String newSql = "select * from (" + sql + ") $_paging_table_limit limit ?, ?";
        // 設置當前sql為修改后的sql
        setSql(metaObject, newSql);

        // 獲取PreparedStatement對象
        PreparedStatement pstmt = (PreparedStatement) invocation.proceed();
        // 獲取sql的總參數個數
        int parameCount = pstmt.getParameterMetaData().getParameterCount();
        // 設置分頁參數
        pstmt.setInt(parameCount - 1, (page.getPageIndex() - 1) * page.getPageSize());
        pstmt.setInt(parameCount, page.getPageSize());

        return pstmt;
    }

    /**
     * 獲取當前查詢的sql
     *
     * @param metaObject
     * @return
     */
    private String getSql(MetaObject metaObject) {
        return (String) metaObject.getValue("delegate.boundSql.sql");
    }

    /**
     * 設置當前查詢的sql
     *
     * @param metaObject
     */
    private void setSql(MetaObject metaObject, String sql) {
        metaObject.setValue("delegate.boundSql.sql", sql);
    }
}

配置分頁插件

mybatis-config.xml配置文件中配置自定義的分頁插件

<plugins>
    <plugin interceptor="cn.xeblog.plugin.PagingPlugin">
        <property name="default.pageIndex" value="1"/>
        <property name="default.pageSize" value="20"/>
    </plugin>
</plugins>

實現DAO

定義POJO對象Role

public class Role {

   private Long id;
   private String roleName;
   private String note;
   // 省略get憾股、set...
}

定義Mapper接口,通過分頁對象查詢角色列表

public interface RoleMapper {
    List<Role> listRoleByPage(Page page);
}

定義Mapper.xml編寫查詢的SQL語句

<mapper namespace="cn.xeblog.mapper.RoleMapper">
    <select id="listRoleByPage" resultType="cn.xeblog.pojo.Role">
        SELECT id, role_name, note FROM role
    </select>
</mapper>

測試分頁插件

測試代碼

@Test
public void test() {
    InputStream inputStream = null;
    SqlSessionFactory sqlSessionFactory;
    SqlSession sqlSession = null;
    try {
        inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sqlSessionFactory.openSession();
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        // 分頁參數箕慧,從第一頁開始服球,每頁顯示5條數據
        Page page = new Page(1, 5);
        List<Role> roleList = roleMapper.listRoleByPage(page);
        System.out.println("===分頁信息===");
        System.out.println("當前頁碼:" + page.getPageIndex());
        System.out.println("每頁顯示數據數:" + page.getPageSize());
        System.out.println("總數據數:" + page.getTotal());
        System.out.println("總頁數:" + page.getTotalPage());
        System.out.println("=============");
        System.out.println("===數據列表===");
        for (Role role : roleList) {
            System.out.println(role);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (sqlSession != null) {
            sqlSession.close();
        }
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

數據庫role表中的全部數據信息

id role_name note
1 SUPER_ADMIN 超級管理員
2 admin 管理員
3 user 用戶
4 user2 用戶2
8 user3 用戶3
9 test 測試
10 test2 測試2
11 test3 測試3
12 test4 測試4
13 test5 測試5

代碼執(zhí)行結果

===分頁信息===
當前頁碼:1
每頁顯示數據數:5
總數據數:10
總頁數:2
=============
===數據列表===
Role{id=1, roleName='SUPER_ADMIN', note=' 超級管理員'}
Role{id=2, roleName='admin', note='管理員'}
Role{id=3, roleName='user', note='用戶'}
Role{id=4, roleName='user2', note='用戶2'}
Role{id=8, roleName='user3', note='用戶3'}

打印的SQL信息

==>  Preparing: select count(*) as total from (SELECT id, role_name, note FROM role) $_paging 
==> Parameters: 
<==    Columns: total
<==        Row: 10
<==      Total: 1
==>  Preparing: select * from (SELECT id, role_name, note FROM role) $_paging_table_limit limit ?, ? 
==> Parameters: 0(Integer), 5(Integer)
<==    Columns: id, role_name, note
<==        Row: 1, SUPER_ADMIN,  超級管理員
<==        Row: 2, admin, 管理員
<==        Row: 3, user, 用戶
<==        Row: 4, user2, 用戶2
<==        Row: 8, user3, 用戶3
<==      Total: 5

參考

  • 《深入淺出MyBatis技術原理與實戰(zhàn)》
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市颠焦,隨后出現的幾起案子斩熊,更是在濱河造成了極大的恐慌,老刑警劉巖伐庭,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粉渠,死亡現場離奇詭異分冈,居然都是意外死亡,警方通過查閱死者的電腦和手機霸株,發(fā)現死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門雕沉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人去件,你說我怎么就攤上這事坡椒。” “怎么了尤溜?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵倔叼,是天一觀的道長。 經常有香客問我宫莱,道長缀雳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任梢睛,我火速辦了婚禮肥印,結果婚禮上,老公的妹妹穿的比我還像新娘绝葡。我一直安慰自己深碱,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布藏畅。 她就那樣靜靜地躺著敷硅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪愉阎。 梳的紋絲不亂的頭發(fā)上绞蹦,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音榜旦,去河邊找鬼幽七。 笑死,一個胖子當著我的面吹牛溅呢,可吹牛的內容都是我干的澡屡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咐旧,長吁一口氣:“原來是場噩夢啊……” “哼驶鹉!你這毒婦竟也來了?” 一聲冷哼從身側響起铣墨,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤室埋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體姚淆,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡孕蝉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了肉盹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡疹尾,死狀恐怖上忍,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情纳本,我是刑警寧澤窍蓝,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站繁成,受9級特大地震影響吓笙,放射性物質發(fā)生泄漏。R本人自食惡果不足惜巾腕,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一面睛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧尊搬,春花似錦叁鉴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冀泻,卻和暖如春常侣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背弹渔。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工胳施, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肢专。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓巾乳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鸟召。 傳聞我的和親對象是個殘疾皇子胆绊,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容