背景
- 项目中有些MybatisSQL莫名的加上了limit,怀疑是使用PageHelper出现分页信息线程间冲突,有的线程使用完分页之后,并没有清除TheadLocal中的分页信息,导致新的请求使用旧的线程使用了旧的分页信息,导致SQL报错
版本
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency>
原理
- PageHelper插件的原理就是在ThreadLocal中埋入分页信息,然后借助Mybatis在同一个线程中通过多个拦截器到达PageHelper,然后对boundSql拼装分页子SQL
- 涉及3个主要Class
- PageHelper
- SqlUtil
- MysqlParser
com.github.pagehelper.PageHelper
- 调用SqlUtil埋入分页信息
- 拦截器。拦截SQL,调用MysqlParser对SQL拼装分页子SQL
package com.github.pagehelper;
@SuppressWarnings("rawtypes")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper implements Interceptor {
..........
/**
* 开始分页
*
* @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 = SqlUtil.getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
// 往ThreadLocal中埋入Page分页信息
SqlUtil.setLocalPage(page);
return page;
}
/**
* Mybatis拦截器方法
*
* @param invocation 拦截器入参
* @return 返回执行结果
* @throws Throwable 抛出异常
*/
public Object intercept(Invocation invocation) throws Throwable {
if (autoRuntimeDialect) {
SqlUtil sqlUtil = getSqlUtil(invocation);
return sqlUtil.processPage(invocation);
} else {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
}
}
com.github.pagehelper.SqlUtil
- setLocalPage埋入分页信息
- clearLocalPage移除分页信息
package com.github.pagehelper;
public class SqlUtil implements Constant {
/**
* Mybatis拦截器方法,这一步嵌套为了在出现异常时也可以清空Threadlocal
*
* @param invocation 拦截器入参
* @return 返回执行结果
* @throws Throwable 抛出异常
*/
public Object processPage(Invocation invocation) throws Throwable {
try {
Object result = _processPage(invocation);
return result;
} finally {
clearLocalPage();
}
}
/**
* 获取Page参数
*
* @return
*/
public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
}
public static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
/**
* 移除本地变量
*/
public static void clearLocalPage() {
LOCAL_PAGE.remove();
}
}
com.github.pagehelper.parser.impl.MysqlParser
- 实现AbstractParser,对于mysql使用MysqlParser,拼接limit分页sql
package com.github.pagehelper.parser.impl;
import com.github.pagehelper.Page;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import java.util.Map;
/**
* @author liuzh
*/
public class MysqlParser extends AbstractParser {
@Override
public String getPageSql(String sql) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
sqlBuilder.append(" limit ?,?");
return sqlBuilder.toString();
}
@Override
public Map<String, Object> setPageParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql, Page<?> page) {
Map<String, Object> paramMap = super.setPageParameter(ms, parameterObject, boundSql, page);
paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
return paramMap;
}
}
问题
- 代码是问题代码,这里PageListFunction接口定义了一个
@FunctionalInterface
函数方法
- function.getPageList则是调用函数方法,如果函数方法中没有经过执行SQL的步骤就直接返回,此时ThreadLocal中的分页信息就不能清除
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.github.pagehelper.SqlUtil;
import com.wf.dev.vo.talent.PageResponseVO;
import java.util.List;
/**
* @author wf
* @date 2021年09月18日 11:19
* @description
*/
public class PageResponseHelper {
/**
* pageHelper抽出方法
*
* @return
*/
public static <T> PageResponseVO<T> getPageResponse(IPage e, PageListFunction function) {
PageHelper.startPage(e.getPageNum(), e.getPageSize());
List<T> analysis = function.getPageList(e);
PageInfo<T> pageInfo = new PageInfo<>(analysis);
return new PageResponseVO<>(pageInfo.getTotal(), pageInfo.getList(), e.getPageNum(), e.getPageSize());
}
}
解决
public class PageResponseHelper {
/**
* pageHelper抽出方法
*
* @return
*/
public static <T> PageResponseVO<T> getPageResponse(IPage e, PageListFunction function) {
try {
PageHelper.startPage(e.getPageNum(), e.getPageSize());
List<T> analysis = function.getPageList(e);
PageInfo<T> pageInfo = new PageInfo<>(analysis);
return new PageResponseVO<>(pageInfo.getTotal(), pageInfo.getList(), e.getPageNum(), e.getPageSize());
} finally {
//清除分页信息
SqlUtil.clearLocalPage();
}
}
}
小结
- 最初使用的时候是知道其中原理,其中执行完SQL后会自动清除,但还是缺少更多特殊情况的考虑
参考