PageHelper底层源码了解一下

5 篇文章 0 订阅

你知道的越多,你不知道的也越多

使用过Mybatis框架的亲故们肯定都听说过PageHelper这个分页神器吧?
简单的一句话PageHelper.startPage(pageNo,pageLimit)就可以帮我们实现分页!
YYDS有没有?废话不多说,开始探索奥秘吧.

日常使用

由于目前很多项目都基于SpringBoot,引入PageHelper也是极其的方便.这里不提供业务代码.相信聪明的你肯定会自行百度,或者直接拿日常项目里的代码作为学习样本.
来来来,我这里随手写了一个demo,主要就是一个分页查询. 上核心代码:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
 /**
     * 依据用户昵称进行模糊分页查询
     *
     * @param name
     * @param page
     * @param limit
     * @return
     */
    public PageInfo<User> findPageUsersByName(String name, int page, int limit) {
        PageHelper.startPage(page, limit);
        List<User> users = userMapper.selectByName(name);
        PageInfo<User> pageUsers = new PageInfo<>(users);
        return pageUsers;
    }
}

对应的UserMapper.xml的文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yuki.mybatisdemo.dao.UserMapper">

    <resultMap id="BaseResultMap" type="com.yuki.mybatisdemo.entity.User">
        <result column="id" jdbcType="INTEGER" property="id" />
        <result column="nickname" jdbcType="VARCHAR" property="nickName" />
        <result column="tel" jdbcType="VARCHAR" property="tel" />
    </resultMap>

    <select id="selectByName" parameterType="java.util.Map" resultMap="BaseResultMap">
        <bind name="nickname" value=" '%'+name+'%' "/>
        select id,nickname,tel from user where nickname like #{nickname}
    </select>
</mapper>

首先先说下,demo里使用的MySQL数据库啊. 因为会涉及到后面底层方言需要选择哪种数据库实现来处理一些逻辑,比如分页sql的拼接方式.

这里我们需要先观察下,进行查询的时候,底层到底执行了哪些SQL:
在这里插入图片描述
额,看出来了, 执行了2条SQL.
奇怪了, UserMapper.xml里的SQL明明只有一条啊,并且根本没有任何的分页参数.
别挠头啦,这就是PageHelper搞的事情嘛~

源码剖析

提前说下实现的原理,方便后续的源码分析阶段.

注意: 这里我使用MySQL数据库!!!

原理:

  • 调用PageHelper.startPage(pageNo,pageLimit)的时候,就已经静悄悄地把我们的分页参数存储到一个变量 ThreadLocal<Page> LOCAL_PAGE;
  • 执行userMapper进行查询,实际上是被一个叫做PageInterceptor.java拦截到了,执行了它重写的interceptor方法. 这里涉及到MyBatis里的拦截器原理,本文不重点说明了,相信聪明你肯定已经知道啦;
  • 该方法里,主要是做了如下事情:
    (1) 获取到MappedStatement, 拿到业务写好的sql, 将sql改造成select count(0) 并执行查询,并将执行结果存到了LOCAL_PAGE里的Page里的total属性.
    (2) 获取到我们自己写在xml里的sql , 并append一些分页sql段,然后执行, 将执行结果存到了LOCAL_PAGE里的Page里的list属性.Page 其实是ArrayList的子类.
  • 可以看出,结果都是封装到了Page里, 最后交由PageInfo,可以获取到总条数,总页数,是否还有下一页等参数.

接下去就是一步一步地验证啦.

1. 分页参数存储

主要是接住了PageHelper.startPage,直接看源码:

    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);
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }

        setLocalPage(page);
        return page;
    }

其实主要就是把我们给的分页参数给到Page,然后实例Page并存储起来,存到哪了?

public abstract class PageMethod {
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();

    public PageMethod() {
    }

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

很显然了,就是ThreadLocal里.

思考下为什么用ThreadLocal ? 想必大家都知道, 可以用来做上下文值传递.
打个比方,在一个Request中,肯定是需要经过多个method处理, 如果多个method都需要使用到某个变量, 就可以放入到ThreadLocal ,各个method想用的时候就get().这样每个method 就不用在入参列表里声明啦!

继续看执行sql到底是怎么做的

2. 拦截器改造SQL
2.1 统计总数

其实肯定是知道通过拦截器底层执行sql的, 对应的拦截器就是PageInterceptor. 先来看下类头部的定义:

@Intercepts({@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class PageInterceptor implements Interceptor {
    protected Cache<String, MappedStatement> msCountMap = null;
    private Dialect dialect;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";
    private Field additionalParametersField;
    private String countSuffix = "_COUNT";

    public PageInterceptor() {
    }

拦截了Executor的query方法,其实也很好理解,毕竟MyBatis 底层查询其实就是借助SqlSession调用Executor#query嘛? 和数据库打交道都是Executor做的事情.

所以如果以后有人问你,拦截的是哪个方法,你可以拍着胸脯告诉他: Executor#query方法.

接下去,我们需要重点看下intercept,解析去采用debug方式,所以直接贴图说明:
在这里插入图片描述
稍微说明下: 这里的Invocation 其实是Plugin#invoke传入来的. 主要就是JDK动态代理的时候, 有三个入参, Plugin 将入参进行二次封装到Invocation里,然后再丢给了Interceptor.
在这里插入图片描述
所以Executor#query 方法的入参都可以在拦截器里拿到,也就意味着,我们可以在拦截器里能拿到当前正执行的MappedStatement(内有SQL)哦~

回到拦截器里,继续看下在哪里进行了总条数查询:
在这里插入图片描述
看下executeAutoCount方法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
总结: UserMapper.xml里解析出来的MappedStatement, 从获取到BoundSql, boundSql.getSql()可以获取到xml里写的sql, 然后交由CCJSqlParserUtil.parse(sql)解析成Statement的一个实现类Select
在这里插入图片描述
Select结构了解下,有一个SelectBody, 同样也有好几个实现类:
在这里插入图片描述
在这里插入图片描述

2.2 分页查询

再回调intercept看下如何分页
在这里插入图片描述
上述统计总条数的逻辑很像:
(1) 改造sql
统计总条数: this.dialect.getCountSql
分页: this.dialect.getPageSql
底层都是先借助PageHelper, 然后再落实到具体的实现类.
Q: 为什么要借助PageHelper?
A: PageHelper算核心接口了,不光要存储分页参数,还要存储结果

(2) executor 执行sql

(3)结果存入到Page

这里重点看下this.dialect.getPageSql
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
回到拦截器方法,看下执行的resultList是否真的存到了ThreadLocal里吧.
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
关注下Page
在这里插入图片描述
其实到这里,源码看的差不多了吧… 等等,既然使用到了ThreadLocal , 常规操作要使用之后,就要remove的啊. 嘿嘿,有的有的. 在 finally
代码块里哦

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

核心类: PageHelper , PageInterceptor, Page .
在这里插入图片描述
在这里插入图片描述

3.PageInfo

那为什么最后还要写一句:PageInfo<User> pageUsers = new PageInfo<>(users)
才能获取到分页信息呢?

在这里插入图片描述

背后思考

  • 如果PageHelper.start写在了mapper查询后面会怎样? mapper查询会按照原sql执行,不会做任何的分页操作;
    这里注意: 由于ThreadLocal是和线程有关的,所以PageHelper.start 也可以跨方法使用.举例子说明:
    在这里插入图片描述

  • 由于ThreadLocal 每次查询的时候都会被remove掉,所以一次mapper查询对应一次PageHelper.start;
    在这里插入图片描述
    对应的sql:
    在这里插入图片描述

  • 14
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
对于PageHelper源码解析,我无法提供详细的源码解析,因为我没有访问和查看源码的权限。但是我可以告诉你一些关于PageHelper的基本信息。 PageHelper是一个用于处理分页的插件,可以在MyBatis中使用。它提供了一些方便的方法来实现分页功能,比如自动拦截SQL并重写为分页SQL,提供了一些额外的分页参数等。 根据引用中的说明,你可以通过在Spring Boot项目中添加pagehelper-spring-boot-starter依赖来引入PageHelper。而在Spring MVC项目中,你需要添加pagehelper依赖。 在使用PageHelper时,你可以使用pageNum参数来指定要查询的页码。引用中提到,如果pageNum小于1,PageHelper会查询第一页;如果pageNum大于总页数,PageHelper会查询最后一页。 具体实现分页功能的方法取决于你的代码结构和框架。引用中给出了一个示例,通过在mapper中调用queryList方法来进行查询。 总之,PageHelper是一个方便的分页插件,它可以简化在数据库中进行分页查询的操作。但是具体的源码解析需要查看PageHelper源码。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [PageHelper源码分析](https://blog.csdn.net/u012125445/article/details/120296170)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值