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 对象传给前端来解析。