从 PageHelper 学到的不侵入 Signature 的 AOP
前言
最近搭新项目框架,之前 Mybatis 的拦截器都是自己写的,一般是有个 Page 类型做判断是否增加分页 sql。但是这样同样的业务开放给页面和 api 可能要写两个,一种带分页类型 Page 一种不带分页。发现开源项目 PageHelper 不需要侵入方法的 Signature 就可以做分页,特此来源码分析一下。
P.S: 看后面的源码分析最好能懂 mybatis 得拦截器分页插件原理
PageHelper 的简答使用
public PageInfo<RpmDetail> listRpmDetailByCondition(String filename, Date startTime, Date endTime,
Integer categoryId, Integer ostypeId, Integer statusId,
Integer pageNo, Integer pageSize) {
PageHelper.startPage(pageNo, pageSize);
List<RpmDetail> result = rpmDetailMapper.listRpmDetailByCondition(filename, startTime, endTime, categoryId,
ostypeId, statusId);
return new PageInfo(result);
}
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;
}
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
可以看出在真正使用分页前,生成了一个 page 对象,然后放入了 ThreadLocal 中,这个思想很巧妙,利用每个请求 Web 服务,每个请求由一个线程处理的原理。利用线程来决定是否进行分页。
是否用 ThreadLocal 来判断是否分页的猜想?
// 调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
// 判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
// 查询总数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
// 处理查询总数,返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
// 当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds 用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
@Override
private PageParams pageParams;
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
if (ms.getId().endsWith(MSUtils.COUNT)) {
throw new RuntimeException(“ 在系统中发现了多个分页插件,请检查系统配置!”);
}
Page page = pageParams.getPage(parameterObject, rowBounds);
if (page == null) {
return true;
} else {
// 设置默认的 count 列
if (StringUtil.isEmpty(page.getCountColumn())) {
page.setCountColumn(pageParams.getCountColumn());
}
autoDialect.initDelegateDialect(ms);
return false;
}
}
/**
* 获取分页参数
*
* @param parameterObject
* @param rowBounds
* @return
*/
public Page getPage(Object parameterObject, RowBounds rowBounds) {
Page page = PageHelper.getLocalPage();
if (page == null) {
if (rowBounds != RowBounds.DEFAULT) {
if (offsetAsPageNum) {
page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
} else {
page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
//offsetAsPageNum=false 的时候,由于 PageNum 问题,不能使用 reasonable,这里会强制为 false
page.setReasonable(false);
}
if(rowBounds instanceof PageRowBounds){
PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
}
} else if(parameterObject instanceof IPage || supportMethodsArguments){
try {
page = PageObjectUtil.getPageFromObject(parameterObject, false);
} catch (Exception e) {
return null;
}
}
if(page == null){
return null;
}
PageHelper.setLocalPage(page);
}
// 分页合理化
if (page.getReasonable() == null) {
page.setReasonable(reasonable);
}
// 当设置为 true 的时候,如果 pagesize 设置为 0(或 RowBounds 的 limit=0),就不执行分页,返回全部结果
if (page.getPageSizeZero() == null) {
page.setPageSizeZero(pageSizeZero);
}
return page;
}
/**
* 获取 Page 参数
*
* @return
*/
public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
}
果然如此,至此真相已经揭开。
怎么保证之后的 sql 不使用分页呢?
如:先查出最近一个月注册的 10 个用户,再根据这些用户查出他们所有的订单。第一次查询需要分页,第二次并不需要
来看看作者怎么实现的?
try{
# 分页拦截逻辑
} finally {
dialect.afterAll();
}
@Override
public void afterAll() {
// 这个方法即使不分页也会被执行,所以要判断 null
AbstractHelperDialect delegate = autoDialect.getDelegate();
if (delegate != null) {
delegate.afterAll();
autoDialect.clearDelegate();
}
clearPage();
}
/**
* 移除本地变量
*/
public static void clearPage() {
LOCAL_PAGE.remove();
}
总结逻辑
将分页对象放入 ThreadLocal
根据 ThreadLocal 判断是否进行分页
无论分页与不分页都需要清除 ThreadLocal
备注
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.8</version>
</dependency>