PageHelper 原理解析

PageHelper 是一款基于 MyBatis 的分页插件,我们只需要在调用 mapper 之前调用 startPage() 方法,传入相应的参数,在调用之后将查询结果封装进 PageInfo 对象中,就能按我们的需要进行分页查询。我们先来看一下如何具体如何使用 PageHelper 插件。

使用步骤

官方网址

https://pagehelper.github.io/

引入 maven 依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.13</version>
</dependency>

在代码用使用

public PageInfo<User> getAllUsers() {
	// 设定当前为第一页,每页大小为 8
    PageHelper.startPage(1, 8);
    List<User> users = userMapper.getAllUsers();
    // 将查到的集合封装到 PageInfo 对象中
    PageInfo<User> userPageInfo = new PageInfo<User>(users);
    return userPageInfo;
}

只需添加短短两行代码,我们就可以得到封装了分页信息的 PageInfo 对象了,下面我们看看它是怎样做到这么简便的。

原理

本文将从 startPage() 方法和 PageInfo 对象角度讲解它的原理。

startPage() 方法

我们先找到 PageHelper 类,发现它继承自 PageMethod

public class PageHelper extends PageMethod implements Dialect {
}

我们再找到它的父类 PageMethod

public abstract class PageMethod {
    
    /**
     * 开始分页
     *
     * @param params
     */
    public static <E> Page<E> startPage(Object params) {
        Page<E> page = PageObjectUtil.getPageFromObject(params, true);
        //当已经执行过orderBy的时候
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page);
        return page;
    }
	
    /**
     * 开始分页
     *
     * @param pageNum      页码
     * @param pageSize     每页显示数量
     * @param count        是否进行count查询
     * @param reasonable   分页合理化,null时用默认配置
     * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page<E>(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;
    }
}

我们在它的父类中,找到了静态方法 startPage(),它有 5 种重载的形式,其余 3 种本质上都是调用了上面 2 种的代码。

我们重点关注一下 setLocalPage() 方法

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

    /**
     * 设置 Page 参数
     *
     * @param page
     */
    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
}

LOCAL_PAGE 是当前线程,通常存储数据在同一个线程中都可以访问到,PageHelper 首先会把我们定义的分页信息封装成 Page 对象,再调用 setLocalPage() 方法,将分页信息保存在当前线程中。

Spring Boot 为我们做了什么

我们找到 PageHelper 的自动配置类

@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageHelperAutoConfiguration {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        //先把一般方式配置的属性放进去
        properties.putAll(pageHelperProperties());
        //在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }

}

我们会发现,Spring Boot 在启动的时候会调用 addPageInterceptor() 方法,为所有的 SqlSessionFactory 添加 PageHelper 的拦截器。

分页插件的使用,首先是在 Mybatis 里面配置了分页拦截器(PageInterceptor),即在执行相关 SQL 之前会拦截做一点事情,所以应该就是在执行selectList的时候,会自动为 SQL 加上limit 1,8

PageInfo 对象

PageInfo 继承自 PageSerializable,我们先看 PageSerializable 类

public class PageSerializable<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    //总记录数
    protected long total;
    //结果集
    protected List<T> list;
}

PageSerializable 类定义了 mapper 查出的结果集合,PageInfo 继承了这个属性,再来看 PageInfo 类

public class PageInfo<T> extends PageSerializable<T> {
    
    public PageInfo() {
    }

    /**
     * 包装Page对象
     *
     * @param list
     */
    public PageInfo(List<T> list) {
        this(list, 8);
    }

    /**
     * 包装Page对象
     *
     * @param list          page结果
     * @param navigatePages 页码数量
     */
    public PageInfo(List<T> list, int navigatePages) {
        super(list);
        if (list instanceof Page) {
            Page page = (Page) list;
            this.pageNum = page.getPageNum();
            this.pageSize = page.getPageSize();

            this.pages = page.getPages();
            this.size = page.size();
            //由于结果是>startRow的,所以实际的需要+1
            if (this.size == 0) {
                this.startRow = 0;
                this.endRow = 0;
            } else {
                this.startRow = page.getStartRow() + 1;
                //计算实际的endRow(最后一页的时候特殊)
                this.endRow = this.startRow - 1 + this.size;
            }
        } else if (list instanceof Collection) {
            this.pageNum = 1;
            this.pageSize = list.size();

            this.pages = this.pageSize > 0 ? 1 : 0;
            this.size = list.size();
            this.startRow = 0;
            this.endRow = list.size() > 0 ? list.size() - 1 : 0;
        }
        if (list instanceof Collection) {
            this.navigatePages = navigatePages;
            //计算导航页
            calcNavigatepageNums();
            //计算前后页,第一页,最后一页
            calcPage();
            //判断页面边界
            judgePageBoudary();
        }
    }
}

它有 3 个构造方法,我们看它的第三个构造方法,它会根据传入的 list 集合类型来进行不同的封装操作。

总结

Spring Boot 在启动的时候,为我们的 SqlSessionFactory 添加了 PageInterceptor,这个拦截器会在 SQL 执行之前添加分页信息,分页信息是通过 Page 对象传递到当前线程中的,查询完成之后,会将查询的结果加上分页信息封装成 PageInfo 对象传给前端来解析。

发布了47 篇原创文章 · 获赞 102 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/MarcoAsensio/article/details/104942041