本文不仅对遇到类似问题的开发者提供了实际的解决思路,也为希望深入理解PageHelper工作机制和多线程编程的读者提供了丰富的技术细节。无论是对于中级开发者还是有经验的架构师,本文的内容都具有一定的参考价值。
1. PageHelper方法使用了静态的ThreadLocal参数,在startPage()调用紧跟MyBatis查询方法后,才会自动清除ThreadLocal存储的对象。
2. 当一个线程先执行了A方法的PageHelper.startPage(int pageNum, int pageSize)后,在未执行到SQL语句前,因为代码抛异常而提前结束。
3. 这个线程被另一个请求复用,根据当前的pageNum和pageSize参数,执行了B方法中的SQL语句。
1. Code Review


先看一下A方法的代码就会发现,在使用了PageHelper.startPage之后,Mybatis查询SQL之前,有很多判断逻辑,并且问题就发生在中间标红的异常情况判断。

a. A方法提前抛异常,且没执行MyBatis查询方法的日志截图

b.B方法执行到MyBatis查询方法的截图



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();List resultList;//调用方法判断是否需要进行分页,如果不需要,直接返回结果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);}return dialect.afterPage(resultList, parameter, rowBounds);} finally {if(dialect != null){dialect.afterAll();}}}
Page page = pageParams.getPage(parameterObject, rowBounds);
Page page = PageHelper.getLocalPage();
resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
为什么不分也得也会拼接?我们回头看下前面提到的dialect.skip(ms, parameterObject, rowBounds):

在intercept方法的最后,会在sql方法执行完成后,清理page缓存:

看看这个afterAll()方法:

只关注 clearPage():

// 针对JSF接口的Filter@Slf4jpublic class BscJsfAspectForPageHelper extends AbstractFilter {public BscJsfAspectForPageHelper(){}@Overridepublic ResponseMessage invoke(RequestMessage requestMessage) {try {log.info("BscJsfAspectForPageHelper.invoke For JSF PageHelper.clearPage()");PageHelper.clearPage();}catch (Exception e){log.error("BscJsfAspectForPageHelper.invoke发生异常,error msg:", e);}return getNext().invoke(requestMessage);}}// XML配置<bean id="bscJsfAspectForPageHelper" class="com.jdl.bsc.aspect.BscJsfAspectForPageHelper" scope="prototype"></bean>
// 针对Controller的切面4jpublic class BscAspectForPageHelper{("execution(public * com.jdl.bsc.controller.*.*(..)) ")public void bscAspectForPageHelper(){}("bscAspectForPageHelper()")public void doBefore(JoinPoint joinPoint) {try {log.info("BscAspectForPageHelper.doBefore For PageHelper.clearPage()");PageHelper.clearPage();}catch (Exception e){log.error("BscAspectForPageHelper.doBefore发生异常,error msg:", e);}}}
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70024922/viewspace-3008804/ ,如需转载,请注明出处,否则将追究法律责任。