Loading

Mybatis分页插件PageHelper安全使用

背景

  • 项目中有些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后会自动清除,但还是缺少更多特殊情况的考虑

参考

posted @ 2023-02-24 13:55  FynnWang  阅读(193)  评论(0编辑  收藏  举报