PageHelper原理

先看一下Page实体类定义,几个重要的属性值。

public class Page<E> extends ArrayList<E> {
    private static final long serialVersionUID = 1L;

    /**
     * 不进行count查询
     */
    private static final int NO_SQL_COUNT = -1;
    /**
     * 进行count查询
     */
    private static final int SQL_COUNT = 0;
    /**
     * 页码,从1开始
     */
    private int pageNum;
    /**
     * 页面大小
     */
    private int pageSize;
    /**
     * 起始行
     */
    private int startRow;
    /**
     * 末行
     */
    private int endRow;
    /**
     * 总数
     */
    private long total;
    /**
     * 总页数
     */
    private int pages;
    /**
     * 分页合理化
     */
    private Boolean reasonable;
    /**
     * 当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
     */
    private Boolean pageSizeZero;
 }
复制代码

Intercepts是mybatis的注解,@Signature是拦截器要拦截的方法签名,

如下:拦截Executor类的query方法,query方法的参数类型有MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class

@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
复制代码

拦截器的实现是实现mybatis的Interceptor接口

public class PageHelper implements Interceptor {
    //sql工具类
    private SqlUtil sqlUtil;
    //属性参数信息
    private Properties properties;
    //自动获取dialect
    private Boolean autoDialect;
 }
复制代码

PageHelper的分页方法,很直观,不赘述。

  /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     */
    public static Page startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, true);
    }

    /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     * @param count    是否进行count查询
     */
    public static Page startPage(int pageNum, int pageSize, boolean count) {
        return startPage(pageNum, pageSize, count, null);
    }

    /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     * @param orderBy  排序
     */
    public static Page startPage(int pageNum, int pageSize, String orderBy) {
        orderBy(orderBy);
        return startPage(pageNum, pageSize);
    }

    /**
     * 开始分页
     *
     * @param pageNum    页码
     * @param pageSize   每页显示数量
     * @param count      是否进行count查询
     * @param reasonable 分页合理化,null时用默认配置
     */
    public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) {
        return startPage(pageNum, pageSize, count, reasonable, null);
    }

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

    /**
     * 开始分页
     *
     * @param params
     */
    public static Page startPage(Object params) {
        Page page = SqlUtil.getPageFromObject(params);
        SqlUtil.setLocalPage(page);
        return page;
    }

    /**
     * 排序
     *
     * @param orderBy
     */
    public static void orderBy(String orderBy) {
        OrderByHelper.orderBy(orderBy);
    }
复制代码

重写mybatis的Interceptor接口的方法,Invocation是JDK动态代理中的InnvocationHandler对象,其传播拦截器链的方法是invocation.proceed();

  /**
     * Mybatis拦截器方法
     *
     * @param invocation 拦截器入参
     * @return 返回执行结果
     * @throws Throwable 抛出异常
     */
    public Object intercept(Invocation invocation) throws Throwable {
        if (autoDialect) {
            initSqlUtil(invocation);
        }
        return sqlUtil.processPage(invocation);
    }
复制代码

其中涉及sqlUtil,把invocation.proceed()包装了一层。先来看sqlUtil的初始化。

如下代码中,一个重要的类MetaObject是mybatis提供的一个用于方便、优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。

此处,通过MetaObject拿到MappedStatement对象引用的DataSource属性,对象引用链为:MappedStatement->Configuration->Environment->Datasource

拿到datasource的url之后封装在SqlUtil中,初始化过程结束。

    /**
     * 初始化sqlUtil
     *
     * @param invocation
     */
    public synchronized void initSqlUtil(Invocation invocation) {
        if (sqlUtil == null) {
            String url = null;
            try {
                MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
                MetaObject msObject = SystemMetaObject.forObject(ms);
                DataSource dataSource = (DataSource) msObject.getValue("configuration.environment.dataSource");
                url = dataSource.getConnection().getMetaData().getURL();
            } catch (SQLException e) {
                throw new RuntimeException("分页插件初始化异常:" + e.getMessage());
            }
            if (url == null || url.length() == 0) {
                throw new RuntimeException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
            }
            String dialect = Dialect.fromJdbcUrl(url);
            if (dialect == null) {
                throw new RuntimeException("无法自动获取数据库类型,请通过dialect参数指定!");
            }
            sqlUtil = new SqlUtil(dialect);
            sqlUtil.setProperties(properties);
            properties = null;
            autoDialect = false;
        }
    }
复制代码

先看一眼sqlUtil类的几个成员变量后面_processPage方法会用到,其中Page对象和COUNT都保存在ThreadLocal中,避免并发问题。

 private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
 private static final ThreadLocal<Boolean> COUNT = new ThreadLocal<Boolean>() {
        @Override
        protected Boolean initialValue() {
            return null;
        }
    };
    private static final Map<String, MappedStatement> msCountMap = new ConcurrentHashMap<String, MappedStatement>();
复制代码

再回过头去看sqlUtil.processPage(invocation)方法调用,最终调用的是com.github.pagehelper.SqlUtil._processPage方法。源码太长了,且源码都有中文注释比较直观,这里只是梳理一下代码逻辑,伪代码如下:

 private Object _processPage(Invocation invocation) throws Throwable {
        final Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds) args[2];
        
        //分支1:如果没有分页对象LocalPage,RowBounds也采取默认值,不执行分页
        if (SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
            //有排序要求,则执行排序拦截器
            if (OrderByHelper.getOrderBy() != null) {
                OrderByHelper.processIntercept(invocation);
            }
            //不需要排序则直接执行原sql
            return invocation.proceed();
        //分支2:需要分页
        } else {
            //获取原始的ms
            MappedStatement ms = (MappedStatement) args[0];
            //判断并处理为PageSqlSource
            if (!isPageSqlSource(ms)) {
                processMappedStatement(ms, parser);
            }
            //忽略RowBounds-否则会进行Mybatis自带的内存分页
            args[2] = RowBounds.DEFAULT;
            //分页信息
            Page page = getPage(rowBounds);
            //分支2.1 判断pageSizeZero,也不需要执行分页
            //pageSizeZero的判断
            if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
                Object result = invocation.proceed();
                return page;
            }
            //分支2.2
            //简单的通过total的值来判断是否进行count查询
            if (page.isCount()) {
                COUNT.set(Boolean.TRUE);
                //替换MS
                args[0] = msCountMap.get(ms.getId());
                //查询总数
                Object result = invocation.proceed();
                //还原ms
                args[0] = ms;
                //设置总数
                page.setTotal((Integer) ((List) result).get(0));
                if (page.getTotal() == 0) {
                    return page;
                }
            }
            
            //分支2.3
            //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
            if (page.getPageSize() > 0 &&
                    ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                            || rowBounds != RowBounds.DEFAULT)) {
                //替换SQL为分页SQL
                BoundSql boundSql = ms.getBoundSql(args[1]);
                args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
                COUNT.set(Boolean.FALSE);
                //执行分页查询
                Object result = invocation.proceed();
                //得到处理结果
                page.addAll((List) result);
            }
            //返回结果
            return page;
        }
    }
复制代码

以上代码总结一下,分支1是不执行分页,ThreadLocal里根本没有分页相关变量;分支2是ThreadLocal有Page对象,分支2又包括三种情况,2.1是设置了PageSizeZero则不进行分页,2.2是需要在原SQL上额外查询表信息总条数,替换了mappedStatement得到count值设置到Page对象里,再invocation.proceed()执行原SQL;2.3是替换SQL为分页SQL返回分页查询结果。不管分不分页,最后都是返回Page对象。

PageHelper最后两个方法也是重写Interceptor的接口方法。

plugin()方法是mybatis提供的生成代理对象的方法。细节参看Plugin.wrap(target, this)。

  /**
     * 只拦截Executor
     *
     * @param target
     * @return
     */
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }
复制代码

setProperties()是从mybatis-config.xml读取属性值。如下xml示例:可以读取到name属性的value值。

<plugins>
    <plugin interceptor="com.example.springbootdemo.mybatis.interceptor.MybatisInterceptor">
            <property name="name" value="test"></property>
    </plugin>
</plugins>
复制代码

主要是设置数据库类型,如下xml:

<!-- 4.0.0以后版本可以不设置该参数 -->
<property name="dialect" value="mysql" />
复制代码
    /**
     * 设置属性值
     *
     * @param p 属性值
     */
    public void setProperties(Properties p) {
        //MyBatis3.2.0版本校验
        try {
            Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0之后新增的类
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("您使用的MyBatis版本太低,MyBatis分页插件PageHelper支持MyBatis3.2.0及以上版本!");
        }
        //数据库方言
        String dialect = p.getProperty("dialect");
        if (dialect == null || dialect.length() == 0) {
            autoDialect = true;
            this.properties = p;
        } else {
            autoDialect = false;
            sqlUtil = new SqlUtil(dialect);
            sqlUtil.setProperties(p);
        }
    }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享