手写MyBatis分页插件,一点也不难

目标

  1. 通过源码分析MyBatis允许被拦截的四大对象
  2. 学习插件原理的同时手写自己的插件

MyBatis插件又称拦截器(Interceptor)

MyBatis使用责任链模式,通过动态代理组织多个插件(拦截器),通过插件可以改变MyBatis的默认行为(例如SQL重写或结果集重写),由于插件会深入到MyBatis的核心,因此在编写自己的插件前最好先了解下它的原理,以便写出安全高效的插件。

MyBatis允许在已映射语句执行过程中的某一点进行拦截调用,默认情况下MyBatis允许使用插件来拦截的四大对象:

  • Executor: 执行增删改查操作
  • StatementHandler: 处理SQL预编译,设置参数等相关工作
  • ParameterHandler: 设置预编译参数用的
  • ResultSetHandler: 处理结果集

允许使用插件来拦截的四大对象在MyBatis的执行流程如下图所示:

MyBatis四大核心接口对象.jpg

从MyBatis的源码中我们可以在Configuration.java文件中看到以上四个对象的创建过程,创建之后会以插件的形式加入到拦截器链

拦截器链.png

实现MyBatis插件

MyBatis定义插件要实现Interceptor接口,这个接口只声明了三个方法:

  • intercept:定义拦截的时候要执行的方法
  • setProperties: 在MyBatis进行配置插件的时候可以配置自定义相关属性
  • plugin: 插件用于封装目标对象,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理,可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,官方默认是:
return Plugin.wrap.(target, this);

这个方法其实是MyBatis简化我们插件实现的工具方法,其实是根据当前拦截的对象创建了一个动态代理对象。

官方推荐使用 @Intercepts 注解来实现拦截器插件的定义,例如:

@Intercepts(
    @Signature(
        type=StatementHandler.class,
        method="prepare",
        args={Connection.class, Integer.class}
    )
)
注解和属性 释义
@Intercepts 只有通过Intercepts注解指定的方法才会执行我们定义的intercept方法
@Signature 定义拦截器需要拦截的方法签名
type 拦截器需要拦截的方法所属的类,上述四大可拦截对象
method 拦截器需要拦截的方法名称
args 拦截器需要拦截的方法参数列表

分页插件思路

基于以上对源码和原理的基本分析,我们来看一下要在MyBatis中实现分页插件要如何做。

由于分页查询需要先知道记录总数,需要先写selectCount获取总数,再根据前端传入的页码(pageNo)和每页记录数(pageSize),得出分页部分的值(以MySQL为例,limit from, to),最后再得到selectPage的完整语句。现在使用分页插件我们想要达到的效果是只通过一个方法就可以实现。

思路:拦截并获取查询的原始SQL,然后拼装成countSQL和pageSQL,再进行查找取值。而如何获取查询的SQL,就需要使用我们上面分析的拦截器来获取了。

分页插件实现

分析完了原理和思路之后,我们看一下具体的实现步骤:

  1. 写一个拦截器用来拦截SQL请求,实现Interceptor接口
  2. 设置拦截器要拦截的类
  3. 重写Interceptor接口的三个方法
  4. 把拼装了分页部分的pageSQL赋给boundSQL
  5. 最后在配置文件中添加拦截器配置

二话不说,先上代码

@Intercepts(
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
)
public class MyPagePlugin implements Interceptor {
   
   // 插件的核心业务
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 把执行流程交给mybatis
        return invocation.proceed();
    }

    // 把自定义的插件加入到mybatis中去执行
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 设置属性
    @Override
    public void setProperties(Properties properties) {
        // TODO 用于适配不同的数据库
        String type = properties.getProperty("type");
    }
}

如上代码,根据前面的分析,我们已经把架子搭好,这部分已经实现了上述步骤的1、2、3,重点和难点是步骤4,我们先对步骤4进一步分解,可以得到:

  1. 拿到原始的SQL语句
seletc * from t_user;
  1. 增加分页子句
seletc * from t_user limit 5, 10;
  1. 基于原始SQL包装查询总数信息
select count(0) from (seletc * from t_user) temp;

梦想照进现实,以上三步对应的代码实现如下

@Override
public Object intercept(Invocation invocation) throws Throwable {
    /**
     * 1、拿到原始的SQL语句 (seletc * from t_user)
     * 2、基于原始SQL包装查询总数信息 (select count(0) from (seletc * from t_user) temp)
     * 3、增加分页子句 (seletc * from t_user limit 5, 10)
     */
     
    // 从invocation拿到StatementHandler对象
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    
    // 从StatementHandler对象拿原始的SQL语句和分页参数
    BoundSql boundSql = statementHandler.getBoundSql();
    String sql = boundSql.getSql();
    Object paramObj = boundSql.getParameterObject();

    // 这一步可以理解为在Spring中使用context.getBean("userBean")
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
    
    // 获取mapper接口中的方法名称,比如:selectUserByPage
    String mapperMethodName = mappedStatement.getId();
    
    // 我们约定只对ByPage结尾的查询进行分页处理
    if (mapperMethodName.matches(".*ByPage$")) {
    
        // 业务层通过Map将参数传进来,分页信息使用PageInfo对象
        Map<String, Object> params = (Map<String, Object>) paramObj;
        PageInfo pageInfo = (PageInfo) params.get("page"); 
        
        // 基于原始SQL(select * from t_user)获取记录总数
        String countSql = "select count(0) from (" + sql + ") temp";
        
        // 为什么这里要使用JDBC ?
        // 应用每一个invocation,只能执行一个SQL语句,要留着执行最后的分页语句
        Connection connection = (Connection) invocation.getArgs()[0];
        PreparedStatement countStatement = connection.prepareStatement(countSql);
        ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
        parameterHandler.setParameters(countStatement);
        ResultSet rs = countStatement.executeQuery();
        if (rs.next()) {
            pageInfo.setTotalNumber(rs.getInt(1));
        }
        rs.close();
        countStatement.close();

        // 增加分页子句
        String pageSql = this.generaterPageSql(sql, pageInfo);

        // 把拼装了分页部分的pageSQL赋给boundSQL
        metaObject.setValue("delegate.boundSql.sql", pageSql);
    }
    
    // 把执行流程交给mybatis
    return invocation.proceed();
}

generaterPageSql 如下:

// 根据原始SQL生成带分页子句的完整语句,以MySQL为例,其他的可以根据类型适配
public String generaterPageSql(String sql, PageInfo pageInfo) {
    StringBuffer sb = new StringBuffer();
    sb.append(sql);
    sb.append(" limit " + pageInfo.getStartIndex() + " , " + pageInfo.getTotalSelect());     
    return sb.toString();
}

最后,将我们的MyPagePlugin配置到MyBatis插件中,即可对ByPage结尾的查询进行分页处理。

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