【源码剖析】PageHelper 源码剖析 + 踩坑分析

插件版本
<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 最常见的使用场景:

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拦截流程

  • 分配线程执行请求;
  • 调用PageHelper.startPage(1, 2)将分页信息写入 ThreadLocal 中;
  • 拦截 SQL 执行,将分页信息写入 SQL;
  • 执行 SQL;
  • 清理ThreadLocal中的分页配置;
  • 线程退出;
  • 线程执行其他请求;

问题总结

结合:ThreadLocal源码剖析

在这里插入图片描述
问题在于:
如果分页信息写入ThreadLocal 之后,语句并没有执行,那么本配置会被带到下一个线程执行的请求中
如果这个中间发生异常,那么已经写入的 ThreadLocal 信息不会被自动清理的;
此时,当前线程被分配进行下一次请求;
page 的参数会被带到下一个请求中;
执行 SQL;
出错;

结论

规范使用:

  • ThreadLocal 在使用之后一定要释放;
  • PageHelper.starPage() 一定要紧跟 SQL语句的执行;
  • 强保证:PageHelper 使用之后一定要 clear()(放在finally中)
  • 26
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值