Backend Development 10 min read

Understanding and Solving ThreadLocal Contamination Issues in MyBatis PageHelper Pagination

This article explains how PageHelper's static ThreadLocal pagination parameters can be polluted by exceptions or thread reuse, leading to incorrect query results, and provides detailed analysis of the plugin's internals together with practical solutions such as invoking clearPage() via filters or aspects.

JD Tech
JD Tech
JD Tech
Understanding and Solving ThreadLocal Contamination Issues in MyBatis PageHelper Pagination

The article investigates a common problem when using MyBatis PageHelper: the static ThreadLocal that stores pagination parameters can remain uncleared if a method calls PageHelper.startPage() and then throws an exception before the SQL execution, causing subsequent requests that reuse the same thread to inherit stale pagination settings.

Problem description (Method A) shows that after startPage(pageNum, pageSize) an exception aborts the SQL, and later Method B, executed on the same thread, runs a full‑table scan but still receives the pagination limits from Method A, producing wrong results.

Inspection steps reveal that PageHelper relies on a ThreadLocal cache set in startPage() and cleared in the finally block of the interceptor. If the finally block is never reached, the cache persists.

Code review of the PageInterceptor.intercept method shows the workflow: retrieve arguments, build CacheKey and BoundSql , call dialect.skip() , possibly execute a count query, then invoke ExecutorUtil.pageQuery for pagination or fall back to executor.query for non‑paginated execution.

Further analysis of PageHelper internals demonstrates how startPage() stores the page in a ThreadLocal via PageHelper.getLocalPage() , how dialect.skip() decides whether to paginate, and how ExecutorUtil.pageQuery performs the actual limited query. The clearPage() method, called in the interceptor's finally block, removes the ThreadLocal entry, and afterAll() performs final cleanup.

The conclusion emphasizes two extreme cases that can break the cleanup: (1) calling startPage() without a subsequent SQL execution, and (2) an exception occurring before the interceptor’s finally block runs. Both leave the ThreadLocal polluted.

To avoid these issues, the article recommends always placing the MyBatis query immediately after startPage() and, when that is not feasible, manually invoking PageHelper.clearPage() before the problematic method. Example implementations are provided:

@Slf4j
public class BscJsfAspectForPageHelper extends AbstractFilter {
    @Override
    public ResponseMessage invoke(RequestMessage requestMessage) {
        try {
            log.info("BscJsfAspectForPageHelper.invoke For JSF PageHelper.clearPage()");
            PageHelper.clearPage();
        } catch (Exception e) {
            log.error("BscJsfAspectForPageHelper.invoke error", e);
        }
        return getNext().invoke(requestMessage);
    }
}
@Aspect
@Component
@Slf4j
public class BscAspectForPageHelper {
    @Pointcut("execution(public * com.jdl.bsc.controller.*.*(..)) ")
    public void bscAspectForPageHelper() {}

    @Before("bscAspectForPageHelper()")
    public void doBefore(JoinPoint joinPoint) {
        try {
            log.info("BscAspectForPageHelper.doBefore For PageHelper.clearPage()");
            PageHelper.clearPage();
        } catch (Exception e) {
            log.error("BscAspectForPageHelper.doBefore error", e);
        }
    }
}

By ensuring the pagination call and the query are tightly coupled or by explicitly clearing the ThreadLocal, developers can prevent unexpected pagination behavior in multi‑threaded environments.

backendJavaMyBatispaginationPageHelperthreadlocal
JD Tech
Written by

JD Tech

Official JD technology sharing platform. All the cutting‑edge JD tech, innovative insights, and open‑source solutions you’re looking for, all in one place.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.