MyBatis-编写自定义分页插件
一、基础知识
1.1 参考方向
- 编写一个分页(Page)基础对象;
 - 基于插件原理,自定义一个分页拦截插件;
 - 基于拦截器,获取BoundSql对象 ,获取动态生成的SQL语句以及相应的参数信息;
 - 根据参数信息,判断是否需要分页查询;
 - 生成统计总数的sql,并查询出总条数;
 - 更新BoundSql对象的数据,设置查询明细sql,加上分页标识;
 - 写好的分页插件配置到MyBatis中;
 
1.2 思考维度
- 生成统计总数语句时,如何保证select count(1)的性能更好;参考方向:详解分页组件中查count总记录优化
 - 当查询出统计总数为零时,有何优雅的办法,不再去查询一次明细信息;
 
二、编码实现
2.1 创建Page对象
- 设置常用分页的基础属性字段;
 - 当不想使用框架默认的自动分页,设置一个可变参数autoCount,可单独查询总数,查询明细组合处理。
 

/**
* 分页类
*
* @author wl
*/
@Data
public class Page implements Serializable {
/**
* 每页显示数量
*/
@JsonProperty("per_page")
private int pageSize;
/**
* 当前页码
*/
@JsonProperty("current_page")
private int curPage;
/**
* 总页数
*/
@JsonProperty("total_pages")
private int pages;
/**
* 总记录数
*/
private int total;
/**
* 当前页数量
*/
private int count;
/**
* 链接
*/
private Link links; /**
* 自动统计分页总数
*/
private boolean autoCount; /**
* 默认无参构造器,初始化各值
*/
public Page() {
this.pageSize = 20;
this.curPage = 1;
this.pages = 0;
this.total = 0;
this.count = 0;
this.autoCount = true;
} public Page(Page page) {
this.pageSize = page.pageSize;
this.curPage = page.curPage;
this.pages = page.pages;
this.total = page.total;
this.count = page.count;
this.links = page.links;
this.autoCount = page.autoCount;
} public void calculate(int total) {
this.setTotal(total);
this.pages = (total / pageSize) + ((total % pageSize) > 0 ? 1 : 0);
// 如果当前页码超出总页数,自动更改为最后一页
//this.curPage = this.curPage > pages ? this.pages : this.curPage;
if (curPage > pages) {
throw new IllegalStateException("超出查询范围");
}
} /**
* 获取分页起始位置和偏移量
*
* @return 分页起始位置和偏移量数组
*/
public int[] paginate() {
// 数量为零时,直接从0开始
return new int[]{total > 0 ? (curPage - 1) * pageSize : 0, pageSize};
}
}
2.2 创建分页插件
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
  // 或者:
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})

/**
* 分页SQL插件
*
* @author wl
* @date 2021-5-26
*/
@Intercepts(
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
)
public class PagePlugin implements Interceptor { @Override
public Object intercept(Invocation invocation) throws Throwable {
// 分页插件拦截处理
useMetaObject(invocation);
return invocation.proceed();
} @Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
} @Override
public void setProperties(Properties properties) {
}
}
2.3 分页拦截函数

private void useMetaObject(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    // 调用MetaObject 反射类处理
    //分离代理对象链
    while (metaObject.hasGetter("h")) {
        Object obj = metaObject.getValue("h");
        metaObject = SystemMetaObject.forObject(obj);
    }
    while (metaObject.hasGetter("target")) {
        Object obj = metaObject.getValue("target");
        metaObject = SystemMetaObject.forObject(obj);
    }
    BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
    // 存在分页标识
    Page page = getPage(boundSql);
    if (Objects.nonNull(page)) {
        int total = getTotalSize(statementHandler, (Connection) invocation.getArgs()[0]);
        if (total <= 0) {
            // 返回数量小于零,查询一个简单的sql,不去执行明细查询 【基于反射,重新设置boundSql】
            metaObject.setValue("delegate.boundSql.sql", "select * from (select 0 as id) as temp where  id>0");
            metaObject.setValue("delegate.boundSql.parameterMappings", Collections.emptyList());
            metaObject.setValue("delegate.boundSql.parameterObject", null);
        } else {
            page.calculate(total);
            String sql = boundSql.getSql() + " limit " + (page.getCurPage() - 1) * page.getPageSize() + ", " + page.getPageSize();
            metaObject.setValue("delegate.boundSql.sql", sql);
        }
    }
}
2.4 辅助函数
判断是否存在page
/***
* 获取分页的对象
* @param boundSql 执行sql对象
* @return 分页对象
*/
private Page getPage(BoundSql boundSql) {
Object obj = boundSql.getParameterObject();
if (Objects.isNull(obj)) {
return null;
}
Page page = null;
if (obj instanceof Page) {
page = (Page) obj;
} else if (obj instanceof Map) {
// 如果Dao中有多个参数,则分页的注解参数名必须是page
try {
page = (Page) ((Map) obj).get("page");
} catch (Exception e) {
return null;
}
}
// 不存在分页对象,则忽略下面的分页逻辑
if (Objects.nonNull(page) && page.isAutoCount()) {
return page;
}
return null;
}
获取统计总数的sql
/***
* 获取统计sql
* @param originalSql 原始sql
* @return 返回统计加工的sql
*/
private String getCountSql(String originalSql) {
// 统一转换为小写
originalSql = originalSql.trim().toLowerCase();
// 判断是否存在 limit 标识
boolean limitExist = originalSql.contains("limit");
if (limitExist) {
originalSql = originalSql.substring(0, originalSql.indexOf("limit"));
}
boolean distinctExist = originalSql.contains("distinct");
boolean groupExist = originalSql.contains("group by");
if (distinctExist || groupExist) {
return "select count(1) from (" + originalSql + ") temp_count";
}
// 去掉 order by
boolean orderExist = originalSql.contains("order by");
if (orderExist) {
originalSql = originalSql.substring(0, originalSql.indexOf("order by"));
}
// todo left join还可以考虑优化
int indexFrom = originalSql.indexOf("from");
return "select count(*) " + originalSql.substring(indexFrom);
}
查询总数
/**
* 查询总记录数
*
* @param statementHandler mybatis sql 对象
* @param conn 链接信息
*/
private int getTotalSize(StatementHandler statementHandler, Connection conn) {
ParameterHandler parameterHandler = statementHandler.getParameterHandler();
String countSql = getCountSql(statementHandler.getBoundSql().getSql());
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = (PreparedStatement) conn.prepareStatement(countSql);
parameterHandler.setParameters(pstmt);
rs = pstmt.executeQuery();
if (rs.next()) {
// 设置总记录数
return rs.getInt(1);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) {
rs.close();
}
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return 0;
}
2.5 运行效果
/**
* 获取进项税信息
*
* @param kid 单号
* @param page 分页参数
* @return 结果
*/
@SelectProvider(type = LifeLogSqlProvider.class, method = "listInputTaxSql")
List<TaxInput> listInputTax(@Param("kid") Integer kid, @Param("page") Page page);
public String listInputTaxSql(@Param("kid") Integer kid, @Param("page") Page page){
    return new SQL()
            .select("input_tax_id, k_id,sup_id,k_sup_id,org_id,a.tax,invoice_title,remark")
            .from("tx_sup_goods_input_tax a")
            .innerJoin("tx_tax b on a.tax=b.tax")
            .where(kid>0,"a.k_id = #{kid}")
            .orderBy("a.k_id desc")
            .build();
}
设置查询接口
/**
* 获取测试税务信息
*
* @return 返回存储数据
*/
@GetMapping("/tax")
public List<TaxInput> listInputTax(int kid, Page page) {
page.setAutoCount(true);
List<TaxInput> taxInputList = lifeLogMapper.listInputTax(kid, page);
if(page.getTotal()==0){
return Collections.emptyList();
}else{
return taxInputList;
}
}

2.6 扩展


private void useReflection(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = statementHandler.getBoundSql();
    // 存在分页标识
    Page page = getPage(boundSql);
    if (Objects.nonNull(page)) {
        int total = getTotalSize(statementHandler, (Connection) invocation.getArgs()[0]);
        if (total <= 0) {
            // 返回数量小于零,查询一个简单的sql,不去执行明细查询 【基于反射,重新设置boundSql】
            Field fieldParameterMappings = BoundSql.class.getDeclaredField("parameterMappings");
            fieldParameterMappings.setAccessible(true);
            fieldParameterMappings.set(boundSql, Collections.emptyList());
            Field fieldSql = BoundSql.class.getDeclaredField("sql");
            fieldSql.setAccessible(true);
            fieldSql.set(boundSql, "select * from (select 0 as id) as temp where  id>0");
            Field fieldParameterObject = BoundSql.class.getDeclaredField("parameterObject");
            fieldParameterObject.setAccessible(true);
            fieldParameterObject.set(boundSql, null);
        } else {
            page.calculate(total);
            Field field = BoundSql.class.getDeclaredField("sql");
            field.setAccessible(true);
            // 设置分页的SQL代码
            field.set(boundSql, boundSql.getSql() + " limit " + (page.getCurPage() - 1) * page.getPageSize() + ", " + page.getPageSize());
        }
    }
}
private void useMetaObjectPlus(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = statementHandler.getBoundSql();
    // 存在分页标识
    Page page = getPage(boundSql);
    if (Objects.nonNull(page)) {
        int total = getTotalSize(statementHandler, (Connection) invocation.getArgs()[0]);
        MetaObject metaObject = SystemMetaObject.forObject(boundSql);
        if (total <= 0) {
            // 返回数量小于零,查询一个简单的sql,不去执行明细查询 【基于反射,重新设置boundSql】
            metaObject.setValue("sql", "select * from (select 0 as id) as temp where  id>0");
            metaObject.setValue("parameterMappings", Collections.emptyList());
            metaObject.setValue("parameterObject", null);
        } else {
            page.calculate(total);
            boolean limitExist = boundSql.getSql().trim().toLowerCase().contains("limit");
            if (!limitExist) {
                String sql = boundSql.getSql() + " limit " + (page.getCurPage() - 1) * page.getPageSize() + ", " + page.getPageSize();
                metaObject.setValue("sql", sql);
            }
        }
    }
}
三、功能扩展
3.1 基于注解配置分页
/***
* 查看注解的自定义插件是否存在
* @param mappedStatement 参数
* @return 返回检查结果
* @throws Throwable 抛出异常
*/
private boolean existEnhancer(MappedStatement mappedStatement) throws Throwable {
//获取执行方法的位置
String namespace = mappedStatement.getId();
//获取mapper名称
String className = namespace.substring(0, namespace.lastIndexOf("."));
//获取方法名aClass
String methodName = namespace.substring(namespace.lastIndexOf(".") + 1);
Class<?> aClass = Class.forName(className);
for (Method method : aClass.getDeclaredMethods()) {
if (methodName.equals(method.getName())) {
// 暂不考虑方法被重载
Enhancer enhancer = method.getAnnotation(Enhancer.class);
if (Objects.nonNull(enhancer) && enhancer.autoPageCount()) {
// 设置page
return true;
}
}
}
return false;
}
3.2 基于查询参数-判断参数是否包含page对象
- 若查询条件中,本身就包括page对象,如何获取page对象?
 - 若查询对象本身继承自Page,如何获取信息page对象?
 
/***
* 获取分页的对象
* @param boundSql 执行sql对象
* @return 分页对象
*/
private Page getPage(BoundSql boundSql) {
Page page = null;
// 参考源码,调试发现为一个map对象
Map<String, Object> parameterList = (Map<String, Object>) boundSql.getParameterObject();
if (Objects.isNull(parameterList)) {
return null;
}
for (Map.Entry<String, Object> entry : parameterList.entrySet()) {
if (entry.getValue() instanceof Page) {
page = (Page) entry.getValue();
break;
}
}
if (Objects.nonNull(page)) {
return page;
}
return null;
}
3.3 插件代码说明
- PageAnnotationExecutorPlugin:表示结合注解,基于Executor.class的query方法做拦截,实现分页功能。
 - PageAnnotationPlugin:表示结合注解,基于StatementHandler.class的prepare方法做拦截,实现分页。该方案主要是调用MetaObject,反射获取对象和设置对象,在不同的代理时,获取到对应对象的模式存在差异(h,target嵌套层不同),存在基于本例获取不到对象的情况。
 - PagePlugin:基于StatementHandler.class的prepare方法做拦截。
 
四、思考总结
- 应当去了解一下比较优秀的MyBatis分页插件,查看源码,学习参考。
 - 若项目允许,还是集成成熟的分页插件,自定义的分页插件难免存在一些不足。
 - 获取类属性时,可基于对象,通过反射获取到对应的类,若对象是基于代理(jdk,cglb)生成的,又该如何获取?
 - 反射可以获取具体执行方法上的注解,获取方法名称,获取参数类型,等具体参考反射的提供的api接口。
 - 当统计总数<1时,是否可以让MyBatis返回一个空集合?暂未找到办法,默认一个简单sql的模式,是一种非主流的方式。
 - 自定义插件的两个关键知识点:MappedStatement,BoundSql。
 - 基于Executor.class和StatementHandler.class在不同的点做拦截时,拦截到的参数不同,获取MappedStatement,BoundSql的方式不同,需查看源码具体分析。
 - 为什么在StatementHandler.class的prepare方法做拦截时,反射重新设置BoundSql对象,就可以更新后续执行的sql信息了,但在Executor.class的query方法做拦截时,反射重新设置BoundSql对象不行,需要重新更新MappedStatement对象?
 - 编写函数时,尽量抽象出通用的辅助函数,每一个辅助函数只做单一的功能。上述三种分页拦截函数实现调整了,都可以使用辅助函数。改动量小。
 - 关于编码规范,强烈推荐书籍:《重构-改善既有代码的设计结构》,《代码整洁之道》。
 
MyBatis-编写自定义分页插件的更多相关文章
- 用jquery编写的分页插件
		
用jquery编写的分页插件 源码 function _pager_go(total_page) { var page_str = $("#_pager_textbox").val ...
 - SpringBoot+Mybatis配置Pagehelper分页插件实现自动分页
		
SpringBoot+Mybatis配置Pagehelper分页插件实现自动分页 **SpringBoot+Mybatis使用Pagehelper分页插件自动分页,非常好用,不用在自己去计算和组装了. ...
 - Mybatis的PageHelper分页插件的PageInfo的属性参数,成员变量的解释,以及页面模板
		
作者:个人微信公众号:程序猿的月光宝盒 //当前页 private int pageNum; //每页的数量 private int pageSize; //当前页的数量 private int si ...
 - MyBatis拦截器自定义分页插件实现
		
MyBaits是一个开源的优秀的持久层框架,SQL语句与代码分离,面向配置的编程,良好支持复杂数据映射,动态SQL;MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyB ...
 - 记一次 IDEA mybatis.generator  自定义扩展插件
		
在使用 idea mybatis.generator 生成的代码,遇到 生成的代码很多重复的地方, 虽然代码是生成的,我们也不应该允许重复的代码出现,因为这些代码后期都要来手动维护. 对于生成时间戳注 ...
 - Springboot 系列(十二)使用 Mybatis 集成 pagehelper 分页插件和 mapper 插件
		
前言 在 Springboot 系列文章第十一篇里(使用 Mybatis(自动生成插件) 访问数据库),实验了 Springboot 结合 Mybatis 以及 Mybatis-generator 生 ...
 - springboot如何集成mybatis的pagehelper分页插件
		
mybatis提供了一个非常好用的分页插件,之前集成的时候需要配置mybatis-config.xml的方式,今天我们来看下它是如何集成springboot来更好的服务的. 只能说springboot ...
 - 【spring boot】14.spring boot集成mybatis,注解方式OR映射文件方式AND pagehelper分页插件【Mybatis】pagehelper分页插件分页查询无效解决方法
		
spring boot集成mybatis,集成使用mybatis拖沓了好久,今天终于可以补起来了. 本篇源码中,同时使用了Spring data JPA 和 Mybatis两种方式. 在使用的过程中一 ...
 - Spring Boot整合tk.mybatis及pageHelper分页插件及mybatis逆向工程
		
Spring Boot整合druid数据源 1)引入依赖 <dependency> <groupId>com.alibaba</groupId> <artif ...
 
随机推荐
- Spring-Cloud-Alibaba之Sentinel
			
微服务中为了防止某个服务出现问题,导致影响整个服务集群无法提供服务的情况,我们在系统访问量和业务量高起来了后非常有必要对服务进行熔断限流处理. 其中熔断即服务发生异常时能够更好的处理:限流是限制每个服 ...
 - 07- HTTP协议详解及Fiddler抓包
			
HTTP协议简介-超文本传输协议 HTTP协议是请求/响应协议:客户端发送请求到服务器,服务器响应该请求.当前版本为1.1版本. HTTP协议特点 1.简单快速:客户向服务器请求服务时,只需传送请求方 ...
 - hdu1074 状态压缩dp+记录方案
			
题意: 给你一些作业,每个作业有自己的结束时间和花费时间,如果超过结束时间完成,一天扣一分,问你把n个作业完成最少的扣分,要求输出方案. 思路: 状态压缩dp,记录方案数的地方 ...
 - hdu 5020 求三点共线的组合数(容器记录斜率出现次数)
			
题意: 给你n个点,问你3点共线的组合数有多少,就是有多少种组合是满足3点共线的. 思路: 一开始抱着试1试的态度,暴力了一个O(n^3),结果一如既往的超时了,然后又在刚刚超时 ...
 - Linux下性能监控、守护进程与计划任务管理
			
目录 一:监视系统进程(ps .top) 二:查看网络连接信息 (netstat) 三:文件进程.端口关联(lsof) 四:计划任务管理(at .crontab) at crontab 一:监视系统进 ...
 - SSH后门万能密码
			
当我们在获得一台Linux服务器的 root 权限后,我们第一想做的就是如何维持这个权限,维持权限肯定想到的就是在目标服务器留下一个后门.但是留普通后门,肯定很容易被发现.我们今天要讲的就是留一个SS ...
 - Python 图片转字符图
			
pip install Image argparse pillow from PIL import Image import argparse #命令行输入参数处理 parser = argparse ...
 - Ext.MessageBox.alert()弹出对话框详解
			
Ext.MessageBox是一个工具类,他继承自Obiect对象,用来生成各种风格的信息提示对话框,Ext.Msg是该类的别名,使用Ext.MessageBox和用Ext.Msg效果是一样的,而后者 ...
 - java之Collection
			
java中的Collection可分为List.Set.Queue三种类型. 1.List. List会按照插入的顺序保存对象,较为常用的实现类有ArrayList,LinkedList和Vector ...
 - 仅用 CSS 实现多彩、智能的阴影
			
背景 有没有想过如何创建从前景元素中继承某些颜色的阴影效果?阅读本文并找出如何实现方法吧! 前几天我经过家得宝(Home Depot,美国家得宝公司,全球领先的家居建材用品零售商),他们正在大规模展销 ...