PageHelper分页插件最新源码解读及使用
相信有很多同学在开发过程中都使用过PageHelper,这是一款强大的分页插件,今天的文章会从以下几个角度来介绍PageHelper,分别为PageHelper的简单介绍使用场景、如何集成到mybatis中以及PageHelper源码解析。
还要在啰嗦一句,如果有使用经验的同学请直接跳转到源码解析~
PageHelper是适用于MyBatis框架的一个分页插件,它支持基本主流与常用的数据库,如MySQL、Oracle、MariaDB、SQLite、Hsqldb等。
PageHelper的使用方式非常便捷,可以在原始SQL查询语句之前添加PageHelper.startPage(pageNum, pageSize);
来启动分页。在查询结束后,通过PageInfo对象可以获取分页信息,如总记录数、总页数、每页大小等。
PageHelper的实现原理基于拦截器(Interceptor),在执行相关SQL之前会拦截并做分页处理。通过ThreadLocal机制,将分页参数保存在当前线程中,确保了分页参数的安全性和准确性。
PageHelper还提供了丰富的配置选项和自定义功能,可以根据实际需求进行灵活配置和使用。例如,可以配置是否支持带有“for update”的查询语句,是否支持嵌套查询等。
总的来说,PageHelper是一款功能强大且易于使用的分页插件,适用于MyBatis框架的分页处理,可以大大简化开发人员的工作量,提高开发效率。
相信很多同学在工作中可能会用到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分页插件来实现
<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
在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);
}
}
这就搞定了,我们就可以得到如下的分页信息
PageHelper的工作原理基于拦截器(Interceptor)。当调用PageHelper.startPage
时,在当前线程上下文中设置一个ThreadLocal变量,用于存储分页参数。在查询执行时,PageHelper 会自动对查询语句进行拦截并进行分页处理。查询结束后,在finally语句中清除ThreadLocal中的查询参数。由于PageHelper方法使用了静态的ThreadLocal参数,分页参数和线程是绑定的,确保了在PageHelper方法调用后紧跟MyBatis查询方法的安全性。
我们点进这个方法里
一直跟一直跟,到这个方法 然后看到有一个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
我们首先找到PageHelperAutoConfiguration类,这个类就是PageHelper在springboot中的自动装配
可以看到上图,创建了一个PageInterceptor的拦截器对象,这个对象实现了Interceptor
然后把这个分页拦截器对象注册到了mybatis的配置文件中
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
StatementHandler
PreparedStatement
对象,并绑定参数。PreparedStatement
紧密相关,负责设置 SQL 语句中的参数。ParameterHandler
PreparedStatement
中。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();
}
}
}
直接看这个page的查询方法
跟进这个方法里去,然后找到dialect.getPageSql()方法跟进去
接着走,找到getPageSql()方法,发现此时的sql还是没有分页的
返回执行到这个方法,this.getPageSql()
然后大家看到了吗?他为我们拼接上了limit语句
然后大家接着看这个方法的返回
最后大家可以看到这个sql的Parameters就是从这里打印的,查询执行的就是这个方法:method.invoke(this.statement, params);
到这里本篇文章就结束了,肯定还有很多不够完善的地方,如果有想到后面再补充。
最后送大家一句话白驹过隙,沧海桑田
文章持续更新,可以关注下方公众号或者微信搜一搜「 迷迭香编程 」获取项目源码、干货笔记、面试题集,第一时间阅读,获取更完整的链路资料。