插件版本
<pagehelper.version>1.4.0</pagehelper.version>
先上结论
规范使用:
- PageHelper.starPage() 一定要紧跟 SQL语句的执行;
- 错误 case1:PageHelper.startPage()之后根本没有 SQL 执行
- 错误 case2:PageHelper.startPage()之后穿插可能报错的业务逻辑、之后才是 SQL 执行
- 强保证:PageHelper 使用之后一定要 clear()(放在finally中)
- ThreadLocal 在使用之后一定要释放;
引子
累计遇到的两个 sql 异常问题:
坑 1:正常sql后面多出无关语句问题排查
坑 2:接口查询为null(偶发性问题)
详见:开发踩坑 专栏
经过初步定位,是和 PageHelper 插件有关,但是具体是什么原因,需要从现象开始、到源码寻找答案:
类图
整个 PageHelper 插件有几个核心的类:
PageHelper类
PageHelper 最常见的使用场景:
PageHelper.startPage(1, 2);
对于PageHelper类本身而言:
/**
* Mybatis - 通用分页拦截器<br/>
* 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper
*
* @author liuzh/abel533/isea533
* @version 5.0.0
*/
public class PageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain {
private PageParams pageParams;
private PageAutoDialect autoDialect;
private PageBoundSqlInterceptors pageBoundSqlInterceptors;
}
这里的三个核心继承:
- PageMethod
- Dialect
- BoundSqlInterceptor.Chain
从核心的方法开始:
PageHelper.startPage(1, 2);
PageMethod类
PageHelper.startPage()实际调用的是PageMethod的startPage方法:
PageMethod#startPage(int, int) :
public abstract class PageMethod {
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static boolean DEFAULT_COUNT = true;
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
*/
public static <E> Page<E> startPage(int pageNum, int pageSize) {
return startPage(pageNum, pageSize, DEFAULT_COUNT);
}
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
return startPage(pageNum, pageSize, count, null, null);
}
/**
* 开始分页
*
* @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;
}
/**
* 设置 Page 参数
*
* @param page
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
}
这里的核心设计:从 ThreadLocal拿到分页信息:
Page<E> oldPage = getLocalPage();
PageMethod类
public abstract class PageMethod {
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static boolean DEFAULT_COUNT = true;
}
核心protected 属性:
- LOCAL_PAGE:ThreadLocal 分页信息
- DEFAULT_COUNT:默认 true;代表是否执行 count 查询;
Page类
public class Page<E> extends ArrayList<E> implements Closeable {
private static final long serialVersionUID = 1L;
/**
* 页码,从1开始
*/
private int pageNum;
/**
* 页面大小
*/
private int pageSize;
/**
* 起始行
*/
private long startRow;
/**
* 末行
*/
private long endRow;
/**
* 总数
*/
private long total;
/**
* 总页数
*/
private int pages;
/**
* 包含count查询
*/
private boolean count = true;
/**
* 分页合理化
*/
private Boolean reasonable;
/**
* 当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
*/
private Boolean pageSizeZero;
/**
* 进行count查询的列名
*/
private String countColumn;
/**
* 排序
*/
private String orderBy;
/**
* 只增加排序
*/
private boolean orderByOnly;
/**
* sql拦截处理
*/
private BoundSqlInterceptor boundSqlInterceptor;
private transient BoundSqlInterceptor.Chain chain;
/**
* 分页实现类,可以使用 {@link com.github.pagehelper.page.PageAutoDialect} 类中注册的别名,例如 "mysql", "oracle"
*/
private String dialectClass;
}
Dialect类
Dialect 包括了核心的方法:
/**
* 数据库方言,针对不同数据库进行实现
*
* @author liuzh
*/
public interface Dialect {
/**
* 跳过 count 和 分页查询
*
* @param ms MappedStatement
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return true 跳过,返回默认查询结果,false 执行分页查询
*/
boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds);
/**
* 执行分页前,返回 true 会进行 count 查询,false 会继续下面的 beforePage 判断
*
* @param ms MappedStatement
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return
*/
boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds);
/**
* 生成 count 查询 sql
*
* @param ms MappedStatement
* @param boundSql 绑定 SQL 对象
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @param countKey count 缓存 key
* @return
*/
String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey);
/**
* 执行完 count 查询后
*
* @param count 查询结果总数
* @param parameterObject 接口参数
* @param rowBounds 分页参数
* @return true 继续分页查询,false 直接返回
*/
boolean afterCount(long count, Object parameterObject, RowBounds rowBounds);
/**
* 处理查询参数对象
*
* @param ms MappedStatement
* @param parameterObject
* @param boundSql
* @param pageKey
* @return
*/
Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey);
/**
* 执行分页前,返回 true 会进行分页查询,false 会返回默认查询结果
*
* @param ms MappedStatement
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return
*/
boolean beforePage(MappedStatement ms, Object parameterObject, RowBounds rowBounds);
/**
* 生成分页查询 sql
*
* @param ms MappedStatement
* @param boundSql 绑定 SQL 对象
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @param pageKey 分页缓存 key
* @return
*/
String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);
/**
* 分页查询后,处理分页结果,拦截器中直接 return 该方法的返回值
*
* @param pageList 分页查询结果
* @param parameterObject 方法参数
* @param rowBounds 分页参数
* @return
*/
Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds);
/**
* 完成所有任务后
*/
void afterAll();
/**
* 设置参数
*
* @param properties 插件属性
*/
void setProperties(Properties properties);
}
PageInterceptor 类
public class PageInterceptor implements Interceptor {
private volatile Dialect dialect;
@Override
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) {
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, null, 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);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}
}
对于继承了 Dialect 的 PageHelper 来说,他实现的afterAll()逻辑:
public class PageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain {
@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(LOCAL_PAGE)的;
ExecutorUtil类
核心的执行类,真正的SQL执行逻辑;
public abstract class ExecutorUtil {
public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql, CacheKey cacheKey) throws SQLException {
//判断是否需要进行分页查询
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分页的缓存 key
CacheKey pageKey = cacheKey;
//处理参数对象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//调用方言获取分页 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey);
}
//执行分页查询
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
}
}
}
整体流程
- 分配线程执行请求;
- 调用PageHelper.startPage(1, 2)将分页信息写入 ThreadLocal 中;
- 拦截 SQL 执行,将分页信息写入 SQL;
- 执行 SQL;
- 清理ThreadLocal中的分页配置;
- 线程退出;
- 线程执行其他请求;
问题总结
问题在于:
如果分页信息写入ThreadLocal 之后,语句并没有执行,那么本配置会被带到下一个线程执行的请求中
如果这个中间发生异常,那么已经写入的 ThreadLocal 信息不会被自动清理的;
此时,当前线程被分配进行下一次请求;
page 的参数会被带到下一个请求中;
执行 SQL;
出错;
结论
规范使用:
- ThreadLocal 在使用之后一定要释放;
- PageHelper.starPage() 一定要紧跟 SQL语句的执行;
- 强保证:PageHelper 使用之后一定要 clear()(放在finally中)