MyBatis插件开发:简单分页插件

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

MyBatis插件开发流程

  • 类实现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);

}

简单分页插件开发

确定拦截的方法签名

需要在实现Interceptor接口的类上加入@Intercepts({@Signature(type, method, args)})注解才能够运行插件。

type-拦截的对象

  • Executor 执行的SQL 全过程,包括组装参数、组装结果返回和执行SQL的过程等都可以拦截。
  • StatementHandler 执行SQL的过程,拦截该对象可以重写执行SQL的过程。
  • ParameterHandler 执行SQL 的参数组装,拦截该对象可以重写组装参数的规则。
  • ResultSetHandler 执行结果的组装,拦截该对象可以重写组装结果的规则。

对于分页插件,我们只需要拦截StatementHandler对象,重写SELECT类型的SQL语句,实现分页功能。

method-拦截的方法

我们已经能够确定拦截的对象是StatementHandler了,现在我们要确定拦截的是哪个方法,因为StatementHandler是通过prepare方法对SQL进行预编译的,所以我们需要对prepare方法进行拦截,在这个方法执行之前,完成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;

  /**
   * 执行更新操作
   *
   * @param statement
   * @return 返回影响行数
   * @throws SQLException
   */
  int update(Statement statement)
      throws SQLException;

  /**
   * 执行查询操作,将结果交给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对象,不执行分页处理,进入责任链下一层
            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();
        // 获取当前需要执行的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);
            //执行查询
            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

代码执行结果

===分页信息===
当前页码: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技术原理与实战》
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容