MyBatis分页插件——PageHelper源码分析

源码下载地址:
https://github.com/pagehelper/Mybatis-PageHelper/tree/25e019186e025aab8b56cd568aea98f583b15fc8

版本:3.7.5

前言:
本文中,类名称和方法名称部分与源码中不同,例:源码:PageHelper(本文:PageUtil);查看代码时,请以源码为准。
内容如有错误或补充,请及时联系、交流。

邮箱地址:[email protected]

一、PageHelper(PageUtil)实例化

①实现org.apache.ibatis.plugin.Interceptor接口,MyBatis初始化时调用setProperties方法,封装sql工具类SqlUtil,同时根据Dialect属性封装具体数据库的Parser类,以及处理MyBatis中MappedStatement的工具类MSUtils(MSUtil)。(Dialect、Parser和MSUtils的封装顺序如下)

PageHelper(PageUtil) :
@Override
public void setProperties(Properties properties) {
    // 获取数据库方言,默认是MySql
    String dialect = properties.getProperty("dialect") == null ? "mysql" : properties.getProperty("dialect");
    sqlUtil = new SqlUtil(dialect);
    sqlUtil.setProperties(properties);
}


SqlUtil :
// 处理org.apache.ibatis.mapping.MappedStatement工具类
private MSUtil msUtil;
// 数据库方言枚举类
private Dialect dialect;
// 具体数据库的Parser
private Parser parser;

/**
 * SqlUtil构造方法
 * @param dialect 数据库方言
 */
public SqlUtil(String strDialect) {
    if (strDialect == null || "".equals(strDialect))
        throw new IllegalArgumentException("Mybatis分页插件无法获取dialect参数!");
    // 根据配置文件中dialect属性,配置数据库方言
    dialect = Dialect.of(strDialect);
    parser = AbstractParser.newParser(dialect);
    msUtil = new MSUtil(parser);
}


AbstractParser :
public abstract class AbstractParser implements Parser, Constant {
    // 处理SQL
    public static final SqlParser sqlParser = new SqlParser();

    public static Parser newParser(Dialect dialect) {
        Parser parser = null;
        switch (dialect) {
            case mysql:
            case mariadb:
            case sqlite:
                parser = new MysqlParser();
                break;
            case oracle:
                parser = new OracleParser();
                break;
            // 此处省略其他数据库实例对象,详见PageHelper源码com.github.pagehelper.parser.impl.AbstractParser
            default:
                break;
        }
        return parser;
    }
}


MSUtils(MSUtil) :
public MSUtil(Parser parser) {
    this.parser = parser;
}

②封装分页属性
在需要分页的查询之前调用PageHelper(PageUtil)的静态方法startPage(int pageNum, int pageSize)方法;封装一个Page(PageBean)对象并保存到当前线程中(ThreadLocal)。

PageHelper(PageUtil) :
/**
 * 开始分页
 * @param pageNum 页码
 * @param pageSize 每页显示数量
 * @param count 是否进行count查询
 * @param reasonable 分页合理化,null时用默认配置
 * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
 * @return
 */
public static PageBean startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    PageBean pageBean = new PageBean(pageNum, pageSize, count);
    pageBean.setReasonable(reasonable);
    pageBean.setPageSizeZero(pageSizeZero);
    // 将PageBean保存至当前线程
    SqlUtil.setLocalPage(pageBean);
    return pageBean;
}

③执行查询方法;调用PageHelper(PageUtil)的实现的plugin(Object target)方法生成代理对象。

PageHelper(PageUtil) :
/**
 * 只拦截Executor
 * @param target
 * @return
 */
@Override
public Object plugin(Object target) {
    if (target instanceof Executor) {
        return Plugin.wrap(target, this);
    } else {
        return target;
    }
}

④调用PageHelper(PageUtil)的实现的intercept(Invocation invocation)开始执行分页。

PageHelper(PageUtil) :
/**
 * MyBatis拦截器方法
 */
@Override
public Object intercept(Invocation invocation) throws Throwable {
    return sqlUtil.processPage(invocation);
}

二、SqlUtil执行分页操作

①调用SqlUtil类的 _processPage(Invocation invocation)执行分页。

SqlUtil :
/**
 * MyBatis拦截器调用方法
 * @param invocation 拦截器入参
 * @return
 * @throws Throwable
 */
public Object processPage(Invocation invocation) throws Throwable {
    try {
        Object result = _processPage(invocation);
        // 返回查询结果
        return result;
    } finally {
        // 清除当前线程的PageBean对象
        clearLocalPageBean();
    }
}

②org.apache.ibatis.plugin.Invocation对象内容。

public class Invocation {
  // org.apache.ibatis.executor.CachingExecutor
  private Object target;
  private Method method;
  private Object[] args;

}

args[0] : org.apache.ibatis.mapping.MappedStatement(描述Mapper配置文件中的一个select/update/insert/delete节点)

args[1] : MapperMethod(存储查询条件值的Map容器)

args[2] : org.apache.ibatis.session.RowBounds(RowBounds用于MyBatis自带的分页功能)

以MySql为例:
获得总记录数count(*) from (sql)
实现分页 sql limit ?, ?

流程:

1、禁用MyBatis自带的分页方法,即将RowBounds对象清空。

2、生成新的MappedStatement对象,将SQL语句(MappedStatement中SqlSource封装了预加载的SQL语句)进行替换,分别增加count(*)和limit ?, ?;并在SqlSource中的List添加分页属性的ParameterMapping对象。

3、在实现分页时,将分页属性封装到args[1]中。

③执行分页方法:

SqlUtil :

/**
 * MyBatis拦截器调用方法
 * @param invocation
 * @return
 * @throws Throwable
 */
private Object _processPage(Invocation invocation) throws Throwable {
    final Object[] args = invocation.getArgs();
    // RowBounds用于MyBatis自带分页功能
    RowBounds rowBounds = (RowBounds)args[2];
    // DEFAULT = new RowBounds()
    if (SqlUtil.getLocalPagebean() == null && rowBounds == RowBounds.DEFAULT) {
        // 没有采用分页
        return invocation.proceed();
    } else {
        // 开始分页
        // 忽略RowBounds,否则会进行MyBatis自带的内存分页
        args[2] = RowBounds.DEFAULT;
        // 分页信息
        PageBean pageBean = getPage(rowBounds);

        if ((pageBean.getPageSizeZero() != null && pageBean.getPageSizeZero()) && pageBean.getPageSize() == 0) {
            // 不进行分页
            Object result = invocation.proceed();
            // 得到处理结果
            pageBean.addAll((List)result);
            pageBean.setPageNum(1);
            pageBean.setPageSize(pageBean.size());
            pageBean.setTotal(pageBean.size());
            return pageBean;
        }

        // 进行分页
        // 获取原始的MappedStatement
        MappedStatement ms = (MappedStatement)args[0];
        // 获得SQL对象
        SqlSource sqlSource = ms.getSqlSource();
        // 通过total值判断是否进行count查询
        if (pageBean.isCount()) {
            // 将参数中的MappedStatement替换为新的qs(count(*)总记录数)
            msUtil.processCountMappedStatement(ms, sqlSource, args);
            // 查询总数
            Object sqlCountResult = invocation.proceed();
            // 设置总数
            pageBean.setTotal((Integer)((List)sqlCountResult).get(0));
            if (pageBean.getTotal() == 0)
                return pageBean;
        }

        // pageSize > 0时执行分页查询,pageSize <= 0时不执行相当于只返回了一个count
        if (pageBean.getPageSize() > 0 &&
                ((rowBounds == RowBounds.DEFAULT && pageBean.getPageNum() > 0)
                        || rowBounds != RowBounds.DEFAULT)) {
            // 将参数中的MappedStatement替换为新的qs(limit分页)
            msUtil.processPageMappedStatement(ms, sqlSource, pageBean, args);
            // 执行分页查询
            Object sqlResult = invocation.proceed();
            // 得到处理结果
            pageBean.addAll((List)sqlResult);
        }
        return pageBean;
    }
}

四、补充MSUtils(MSUtil)和Parser类

SqlUtil获得总记录数和执行分页查询时,分别调用了MSUtils(MSUtil)的processCountMappedStatement(MappedStatement ms, SqlSource sqlSource, Object[] args)和processPageMappedStatement(MappedStatement mappedStatement, SqlSource sqlSource, PageBean pageBean, Object[] args)。

①计算总记录数processCountMappedStatement方法:

MSUtils(MSUtil) :
/**
 * 处理count查询的MappedStatement
 * @param ms
 * @param sqlsource
 * @param args
 */
public void processCountMappedStatement(MappedStatement ms, SqlSource sqlSource, Object[] args) {
    args[0] = getMappedStatement(ms, sqlSource, args[1], SUFFIX_COUNT);
}

1、根据ID + 后缀(MappedStatement的id属性 + ‘_PageHelper_Count’)查询缓存,是否有MappedStatement对象。

2、若获取缓存失败,则需要创建新的MappedStatement对象。取出具体SqlSource的实现类(MSUtils的getSqlSource(MappedStatement mappedStatement, SqlSource sqlSource, Object parameterObject, boolean count)方法)。统计总记录数时属于StaticSqlSource类。

3、修改SQL语句,调用Parser实现类的getCountSql(String sql_count)方法(实际com.github.pagehelper.parser.SqlParser类的getSmartCountSql(String sql)方法);得到count(*) from (sql)的语句,封装至StaticSqlSource对象并返回。

MSUtils(MSUtil) :
/**
 * 获取静态count的SqlSource
 * @param configuration
 * @param sqlSource
 * @param parameterObject
 * @return
 */
public SqlSource getStaticCountSqlSource(Configuration configuration, SqlSource sqlSource, Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    return new StaticSqlSource(configuration, parser.getCountSql(boundSql.getSql()), boundSql.getParameterMappings());
}

4、根据新生成的StaticSqlSource对象,创建一个新的MappedStatement对象(MSUtils的newMappedStatement(MappedStatement ms, SqlSource sqlSource, String suffix)方法);并保存到缓存。

5、将新的MappedStatement替换org.apache.ibatis.plugin.Invocation的args[0]。

6、执行查询。

②分页查询processPageMappedStatement方法:

MSUtils(MSUtil) :
/**
 * 处理分页查询的MappedStatement
 * @param ms
 * @param sqlSource
 * @param pageBean
 * @param args
 */
public void processPageMappedStatement(MappedStatement mappedStatement, SqlSource sqlSource, PageBean pageBean, Object[] args) {
    args[0] = getMappedStatement(mappedStatement, sqlSource, args[1], SUFFIX_PAGE);
    // 处理入参
    args[1] = setPageParameter((MappedStatement) args[0], args[1], pageBean);
}

1、根据ID + 后缀(MappedStatement的id属性 + ‘_PageHelper’)查询缓存,是否有MappedStatement对象。

2、若获取缓存失败,则需要创建新的MappedStatement对象。取出具体SqlSource的实现类(MSUtils的getSqlSource(MappedStatement mappedStatement, SqlSource sqlSource, Object parameterObject, boolean count)方法)(动态SQL:PageUtilDynamicSqlSource;注解式SQL:PageProviderSqlSource或静态SQL:StaticPageSqlSource)。
以静态SQL对象为例:

MSUtils(MSUtil) :
/**
 * 获取静态的分页SqlSource
 * ParameterMappings需要增加分页参数的映射
 * @param configuration
 * @param sqlSource
 * @param parameterObject
 * @return
 */
public SqlSource getStaticPageSqlSource(Configuration configuration, SqlSource sqlSource, Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    return new StaticSqlSource(configuration, parser.getPageSql(boundSql.getSql()), parser.getPageParameterMapping(configuration, boundSql));
}

3、修改SQL语句,调用Parser实现类的getPageSql(String sql)方法(根据Dialect属性实现的子类,比如针对MySql方言的com.github.pagehelper.parser.impl.MysqlParser类);在原查询语句的末尾添加’limit ?,?’。
以MySql方言为例:

MySqlParser :
@Override
public String getPageSql(String sql) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    sqlBuilder.append(sql);
    sqlBuilder.append(" limit ?,?");
    return sqlBuilder.toString();
}

4、由于增加条件参数,对比统计总记录数的方法,需要讲SqlSource中的List进行替换,并添加分页属性的ParameterMapping对象。

Contant(用于存储常量) :
// 分页的ID后缀
String SUFFIX_PAGE = "_PageHelper";
// 第一个分页参数
String PAGEPARAMETER_FIRST = "First" + SUFFIX_PAGE;
// 第二个分页参数
String PAGEPARAMETER_SECOND = "Second" + SUFFIX_PAGE;

AbstractParser :
public List<ParameterMapping> getPageParameterMapping(Configuration configuration, BoundSql boundSql) {
    List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>();
    if (boundSql.getParameterMappings() != null) {
        newParameterMappings.addAll(boundSql.getParameterMappings());
    }
    newParameterMappings.add(new ParameterMapping.Builder(configuration, PAGEPARAMETER_FIRST, Integer.class).build());
    newParameterMappings.add(new ParameterMapping.Builder(configuration, PAGEPARAMETER_SECOND, Integer.class).build());
    return newParameterMappings;
}

5、根据新生成的SqlSource对象,创建一个新的MappedStatement对象(MSUtils的newMappedStatement(MappedStatement ms, SqlSource sqlSource, String suffix)方法);并保存到缓存。

6、将新的MappedStatement替换org.apache.ibatis.plugin.Invocation的args[0]。

7、处理入参(MSUtils的setPageParameter(MappedStatement mappedStatement, Object parameterObject, PageBean pageBean)方法)。

MSUtils(MSUtil) :
/**
 * 设置分页参数 limit ? ?
 * @param mappedStatement
 * @param object
 * @param pageBean
 * @return
 */
private Map setPageParameter(MappedStatement mappedStatement, Object parameterObject, PageBean pageBean) {
    BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
    return parser.setPageParameter(mappedStatement, parameterObject, boundSql, pageBean);
}

以MySql方言为例:

MySqlParser :
@Override
public Map setPageParameter(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql, PageBean pageBean) {
    // MySqlParser 继承 AbstractParser
    Map paramMap = super.setPageParameter(mappedStatement, parameterObject, boundSql, pageBean);
    paramMap.put(PAGEPARAMETER_FIRST, pageBean.getStartRow());
    paramMap.put(PAGEPARAMETER_SECOND, pageBean.getPageSize());
    return paramMap;
}

AbstractParser的setPageParameter(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql, PageBean pageBean)方法详见源码com.github.pagehelper.parser.impl.AbstractParser.setPageParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql, Page page)。

8、将新的Map替换org.apache.ibatis.plugin.Invocation的args[1]。

9、执行查询。

猜你喜欢

转载自blog.csdn.net/qq_33755556/article/details/81196558