PageHelper分页插件最新源码解读及使用
相信有很多同学在开发过程中都使用过PageHelper,这是一款强大的分页插件,今天的文章会从以下几个角度来介绍PageHelper,分别为PageHelper的简单介绍使用场景、如何集成到mybatis中以及PageHelper源码解析。
还要在啰嗦一句,如果有使用经验的同学请直接跳转到源码解析~
1、PageHelper介绍
PageHelper是适用于MyBatis框架的一个分页插件,它支持基本主流与常用的数据库,如MySQL、Oracle、MariaDB、SQLite、Hsqldb等。
PageHelper的使用方式非常便捷,可以在原始SQL查询语句之前添加PageHelper.startPage(pageNum, pageSize);
来启动分页。在查询结束后,通过PageInfo对象可以获取分页信息,如总记录数、总页数、每页大小等。
PageHelper的实现原理基于拦截器(Interceptor),在执行相关SQL之前会拦截并做分页处理。通过ThreadLocal机制,将分页参数保存在当前线程中,确保了分页参数的安全性和准确性。
PageHelper还提供了丰富的配置选项和自定义功能,可以根据实际需求进行灵活配置和使用。例如,可以配置是否支持带有“for update”的查询语句,是否支持嵌套查询等。
总的来说,PageHelper是一款功能强大且易于使用的分页插件,适用于MyBatis框架的分页处理,可以大大简化开发人员的工作量,提高开发效率。
2、PageHelper集成
相信很多同学在工作中可能会用到mybatis-plus,直接调用Mapper接口的selectPage方法就可以了实现分页了,那为啥还要用插件呢
比如:
@Override public Page<TaskAuth> pageTaskAuthVO(Page<TaskAuth> page, TaskAuthDTO taskAuthDTO) { QueryWrapper<TaskAuth> wrapper = new QueryWrapper<>(); wrapper.eq("status",0); if (StringUtils.isNotEmpty(taskAuthDTO.getSysCode())){ wrapper.like("sys_code",taskAuthDTO.getSysCode()); } if (StringUtils.isNotEmpty(taskAuthDTO.getAuthFlag())){ wrapper.eq("auth_flag",taskAuthDTO.getAuthFlag()); } if (StringUtils.isNotEmpty(taskAuthDTO.getClientId())){ wrapper.like("client_id",taskAuthDTO.getClientId()); } Page<TaskAuth> selectedPage = taskAuthMapper.selectPage(page, wrapper); return selectedPage; }
上述代码直接调用taskAuthMapper.selectPage() 返回分页信息;
但是我们可以看到上述方法好像仅支持单表查询,QueryWrapper wrapper = new QueryWrapper<>(); 限定了Wrapper的泛型。
所以如果碰见几个表那种的关联查询还需要分页的话就比较麻烦,那怎么办呢,解决办法就是使用PageHelper分页插件来实现
2.1 引入PageHelper依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>2.1.0</version> <exclusions> <exclusion> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> </exclusion> <exclusion> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> </exclusion> </exclusions> </dependency>
有同学可以看到上述依赖排除了mybatis的相关配置,是因为我项目中也引入了mybatis-plus 会有依赖冲突,所以就排除了
建议大家还是使用相对较新的版本 我这里使用的1.4.7版本
大家也可以直接去maven官方仓库搜索 地址给大家贴这里了 https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter
2.2 PageHelper的使用
在Mapper.xml中实现自己的sql
我这里演示一个关联查询的
<select id="pageTaskSysVO" resultType="com.demo.center.vo.TaskSysVO"> select tg.id , tg.src_code , tg.task_group_name , tg.task_group_img_url , tg.msg_flag_memo, tg.create_time , tg.src_codes , tg.sys_type , ta.auth_flag , ta.client_id from task_group tg left join task_auth ta on tg.src_code = ta.sys_code where 1=1 <if test="srcCode != null and srcCode != ''"> and tg.src_code = #{srcCode} </if> <if test="taskGroupName != null and taskGroupName != ''"> and tg.task_group_name like CONCAT('%',#{taskGroupName},'%') </if> <if test="authFlag != null and authFlag != ''"> and ta.auth_flag = #{authFlag} </if> <if test="clientId != null and clientId != ''"> and ta.client_id = #{clientId} </if> <if test="id != null and id != ''"> and tg.id = #{id} </if> and tg.p_task_group_code = '1' order by create_time desc </select>
Mapper接口的实现
List<TaskSysVO> pageTaskSysVO(TaskSysDTO taskSysDTO);
service实现类实现
@Override public Page<TaskSysVO> pageTaskSysVO(TaskSysDTO taskSysDTO) { if (taskSysDTO.getCurrent() == null){ throw new TaskCenterException("分页数据不能为空"); } if (taskSysDTO.getSize() == null){ throw new TaskCenterException("分页数据不能为空"); } Page<TaskSysVO> page = new Page<>(); PageHelper.startPage(Integer.valueOf(taskSysDTO.getCurrent().toString()), Integer.valueOf(taskSysDTO.getSize().toString())); List<TaskSysVO> voList = taskGroupMapper.pageTaskSysVO(taskSysDTO); page.setTotal(new PageInfo(voList).getTotal()); page.setRecords(voList); return page; }
主要代码为:PageHelper.startPage();
注意上述代码中,我把得到的分页信息给到了Page这个对象,是因为我得业务中用到的是这个分页对象, 大家可以直接返回 new PageInfo(voList);
下面给一个示例:
@Service public class UserService { @Autowired private UserMapper userMapper; public PageInfo<User> findAll(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<User> users = userMapper.findAll(); return new PageInfo<>(users); } }
这就搞定了,我们就可以得到如下的分页信息
3、PageHelper源码解读
3.1 PageHelper的工作原理
PageHelper的工作原理基于拦截器(Interceptor)。当调用PageHelper.startPage
时,在当前线程上下文中设置一个ThreadLocal变量,用于存储分页参数。在查询执行时,PageHelper 会自动对查询语句进行拦截并进行分页处理。查询结束后,在finally语句中清除ThreadLocal中的查询参数。由于PageHelper方法使用了静态的ThreadLocal参数,分页参数和线程是绑定的,确保了在PageHelper方法调用后紧跟MyBatis查询方法的安全性。
3.2 PageHelper.startPage() 方法入口
我们点进这个方法里
一直跟一直跟,到这个方法 然后看到有一个setLocalPage(page);的方法
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); //当已经执行过orderBy的时候 Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } setLocalPage(page); return page; }
点进去,可以看到已经把分页对象set到了当前线程的上下文中
下面就来了解下sql是如何被拦截并且加上了分页语句
我这里专门下载了 pagehelper-spring-boot-starter 1.4.7 的源码包
感兴趣的同学可以点从这里下载:https://github.com/pagehelper/Mybatis-PageHelper
3.3 PageHelper初始化
我们首先找到PageHelperAutoConfiguration类,这个类就是PageHelper在springboot中的自动装配
可以看到上图,创建了一个PageInterceptor的拦截器对象,这个对象实现了Interceptor
然后把这个分页拦截器对象注册到了mybatis的配置文件中
3.4 那是怎么拦截的哩
mybatis在执行mapper方法时,创建Executor,执行pluginAll
方法,然后会进入Interceptor
的实现类PageInterceptor
的plugin
方法。我们来看下PageInterceptor
:
下面标一下整个流程的主要方法
当我们执行mapper时会进入MapperProxy的invoke方法
接下来会通过sqlSession执行查询方法
然后会创建一个执行sql语句的插件Executor
在接着跟代码,添加插件到拦截器执行链
这里使用的时候都是用动态代理将多个插件用责任链的方式添加的,最后返回的是一个代理对象,拿到分页插件的拦截器
最后动态代理生成和调用的过程都在 Plugin.wrap中
可以看到这个执行器对应了两个查询,方法返回的jdk动态代理类(增强了目标类)
public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 获取map key就是插件,value就是查询方法 Class<?> type = target.getClass(); // 拦截目标(ParameterHandler|ResultSetHandler|StatementHandler|Executor) Class<?>[] interfaces = getAllInterfaces(type, signatureMap);// 获取目标接口 // 生成代理 return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target; }
然后代码会进入invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass()); // 取出拦截的目标方法 // 判断这个方法是否在拦截范围内,在就拦截,不在就调用方法本身 return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args); } catch (Exception var5) { throw ExceptionUtil.unwrapThrowable(var5); } }
mybatis添加插件的流程大致为:
mybatis 插件的拦截目标有四个,分别为 Executor、StatementHandler、ParameterHandler、ResultSetHandler 下面讲一下这四个插件的作用及区别:
- Executor
- 作用:负责实际的 SQL 语句执行,包括事务管理和 SQL 语句的执行。
- 区别:是核心插件,其他三个插件是为它服务的。
- StatementHandler
- 作用:负责创建 JDBC 的
PreparedStatement
对象,并绑定参数。 - 区别:它与 JDBC 的
PreparedStatement
紧密相关,负责设置 SQL 语句中的参数。
- ParameterHandler
- 作用:负责设置参数到 JDBC 的
PreparedStatement
中。 - 区别:它处理的是 SQL 语句中的参数部分,而
StatementHandler
负责的是整个 SQL 语句。
- ResultSetHandler
- 作用:负责处理从数据库返回的结果集。
- 区别:它处理的是从数据库查询返回的结果集,将结果集转换成 Java 对象。
最后是通过上面的invoke方法判断方法类型进入到了 PageInterceptor 也就是分页插件拦截器
方法大体如下:
public Object intercept(Invocation invocation) throws Throwable { try { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement)args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds)args[2]; ResultHandler resultHandler = (ResultHandler)args[3]; Executor executor = (Executor)invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; if (args.length == 4) { boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { cacheKey = (CacheKey)args[4]; boundSql = (BoundSql)args[5]; } this.checkDialectExists(); if (this.dialect instanceof BoundSqlInterceptor.Chain) { boundSql = ((BoundSqlInterceptor.Chain)this.dialect).doBoundSql(Type.ORIGINAL, boundSql, cacheKey); } List resultList; //调用方法判断是否需要进行分页,如果不需要,直接返回结果 if (!this.dialect.skip(ms, parameter, rowBounds)) { this.debugStackTraceLog(); //判断是否需要进行 count 查询 if (this.dialect.beforeCount(ms, parameter, rowBounds)) { Long count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql); if (!this.dialect.afterCount(count, parameter, rowBounds)) { Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds); return var12; } } resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds); return var16; } finally { if (this.dialect != null) { this.dialect.afterAll(); } } }
3.5 分页语句是怎么拼接到查询后面的呢
直接看这个page的查询方法
跟进这个方法里去,然后找到dialect.getPageSql()方法跟进去
接着走,找到getPageSql()方法,发现此时的sql还是没有分页的
返回执行到这个方法,this.getPageSql()
然后大家看到了吗?他为我们拼接上了limit语句
然后大家接着看这个方法的返回
最后大家可以看到这个sql的Parameters就是从这里打印的,查询执行的就是这个方法:method.invoke(this.statement, params);
到这里本篇文章就结束了,肯定还有很多不够完善的地方,如果有想到后面再补充。
最后送大家一句话白驹过隙,沧海桑田