PageHelper实现分页原理

104 阅读7分钟

分页使用

PageUtils.startPage(pageNum, pageSize);
List<GydnSharedFileUserDto> gydnSharedFileUserDtos = gydnSharedFilesMapper.selectSharedFiles(userId);
long total = new PageInfo<>(gydnSharedFileUserDtos).getTotal();

像上面的短短三行代码,就实现了分页查询,并且获得了查询的总条数,接下来我来讲讲其怎么实现分页查询的

startPage()

进入该方法查看:

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<E> page = new Page(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    Page<E> oldPage = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }

    setLocalPage(page);
    return page;
}

该方法首先会创建一个Page的对象,Page类如以下代码,继承了ArrayList集合,这点我们要记住

public class Page<E> extends ArrayList<E> implements Closeable {
    private static final long serialVersionUID = 1L;
    private static final Log log = LogFactory.getLog(Page.class);
    private final String stackTrace;
    private int pageNum;
    private int pageSize;
    private long startRow;
    private long endRow;
    private long total;
    private int pages;
    private boolean count;
    private Boolean reasonable;
    private Boolean pageSizeZero;
    private String countColumn;
    private String orderBy;
    private boolean orderByOnly;
    private BoundSqlInterceptor boundSqlInterceptor;
    private transient Chain chain;
    private String dialectClass;
    private Boolean keepOrderBy;
    private Boolean keepSubSelectOrderBy;
在该方法中创建Page对象之后,由于Page是存储在ThreadLocal中,因此每一个线程都有一个Page对象,进行分页时,会获取线程当前的Page对象,进行设置,最后讲新的Page对象放到ThreadLocal中
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
protected static boolean DEFAULT_COUNT = true;

public PageMethod() {
}

protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

查询拦截源码解析

当执行mapper方法时,会直接调用mapper的方法吗?不会,其不仅仅会生成mapper代理,走一些缓存还有一些参数处理逻辑,其还会被拦截,走mybatis的拦截逻辑。

PageHelper的拦截器如以下代码,首先实现Interceptor接口,通过@Intercepts注解以及@Signature注解标记要拦截方法。

@Intercepts({@Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class PageInterceptor implements Interceptor {

当执行相应的方法时,就会走其intercept方法:

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) {
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            cacheKey = (CacheKey)args[4];
            boundSql = (BoundSql)args[5];
        }

        this.checkDialectExists();
        if (this.dialect instanceof Chain) {
            boundSql = ((Chain)this.dialect).doBoundSql(Type.ORIGINAL, boundSql, cacheKey);
        }

        List resultList;
        //当分页参数不为null时
        if (!this.dialect.skip(ms, parameter, rowBounds)) {
            this.debugStackTraceLog();
            if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                Long count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql);
                if (!this.dialect.afterCount(count, parameter, rowBounds)) {
                    Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    return var12;
                }
            }

            resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }

        Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds);
        return var16;
    } finally {
        if (this.dialect != null) {
            this.dialect.afterAll();
        }

    }
}

这个方法很长,我讲讲其几个重要的方法实现:

//查询数据的总条数
Long count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql);
//讲条数设置当前线程的Page对象中,如果当前分页数量小于0或者总条数小于起始行,不进行以下的分页查询,提高性能
if (!this.dialect.afterCount(count, parameter, rowBounds)) {
//分页查询,进行sql拼接,调用Executor执行sql
resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
//设置Page,直接返回
Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds);
return var16;
public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
//获取当前线程的Page对象
    Page page = this.getLocalPage();
    //讲总条数设置到Page对象中
    page.setTotal(count);
    if (rowBounds instanceof PageRowBounds) {
        ((PageRowBounds)rowBounds).setTotal(count);
    }

    if (page.getPageSizeZero() != null) {
        if (!page.getPageSizeZero() && page.getPageSize() <= 0) {
            return false;
        }

        if (page.getPageSizeZero() && page.getPageSize() < 0) {
            return false;
        }
    }

    return page.getPageNum() > 0 && count > page.getStartRow();
}

在获取总条数之后,会判断可不可以进行分页操作: 在分页查询时,会对sql进行分页参数的拼接,最后由Executor执行sql

public String getPageSql(String sql, Page page, CacheKey pageKey) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    sqlBuilder.append(sql);
    if (page.getStartRow() == 0L) {
        sqlBuilder.append("\n LIMIT ? ");
    } else {
        sqlBuilder.append("\n LIMIT ?, ? ");
    }

    return sqlBuilder.toString();
}

在sql执行结束之后:

会调用以下方法处理返回的集合,最后讲得到的结果返回:Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds);

public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
//获取当前线程的Page对象,在上文中Page对象已经被设置了总条数
    Page page = this.getLocalPage();
    if (page == null) {
        return pageList;
    } else {
   
        page.addAll(pageList);
        if (!page.isCount()) {
            page.setTotal(-1L);
        } else if (page.getPageSizeZero() != null && page.getPageSizeZero() && page.getPageSize() == 0) {
            page.setTotal((long)pageList.size());
        } else if (page.isOrderByOnly()) {
            page.setTotal((long)pageList.size());
        }

        return page;
    }
}

在该方法中,会获取Page对象,由于Page对象继承了ArrayList,其自然包含了Object数组,将分页查询得到的列表数据放入Page的集合中,最后直接返回这个Page对象

所以我们调用mapper方法返回的结果其实是一个Page对象。而Page对象又继承了ArrayList,自然可以用List类型来表示,这是多态的实现。

获取分页数据

来看 new PageInfo<>(gydnSharedFileUserDtos)这个方法,如果我们刚刚用的时候,看到肯定会很懵逼,不知道为什么传入mapper方法的结果就可以获取分页的数据和总条数,但是现在,其实我们应该都可以理解,正如我们上面刚刚说的,mapper返回的结果就是一个Page对象,而Page对象我们把总条数、分页数据以及其他数据都放了进去。想要访问其数据,就直接把上一把得到的结果强转即可。

public PageSerializable(List<? extends T> list) {
    this.list = list;
    if (list instanceof Page) {
        this.total = ((Page)list).getTotal();
    } else {
        this.total = (long)list.size();
    }

}

Mybatis拦截器拦截原理

PageInterceptor实现了Interceptor接口,并且其类上存在相关注解,其实现原理如下: 当使用mybatis时,首先会从mybatis-config.xml中读取相关配置,利用XmlConfigBuilder解析每一个标签结点,存在方法解析插件:

private void parseConfiguration(XNode root) {
    try {
        this.propertiesElement(root.evalNode("properties"));
        Properties settings = this.settingsAsProperties(root.evalNode("settings"));
        this.loadCustomVfs(settings);
        this.loadCustomLogImpl(settings);
        this.typeAliasesElement(root.evalNode("typeAliases"));
        this.pluginElement(root.evalNode("plugins"));
        this.objectFactoryElement(root.evalNode("objectFactory"));
        this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
        this.settingsElement(settings);
        this.environmentsElement(root.evalNode("environments"));
        this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        this.typeHandlerElement(root.evalNode("typeHandlers"));
        this.mapperElement(root.evalNode("mappers"));
    } catch (Exception var3) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
    }
}

该方法内会解析拦截器,我们在使用Spring时就可以将拦截器放到指定标签中,然后进行解析时会获取标签下的所有拦截器并且放入interceptorChain中。

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        Iterator var2 = parent.getChildren().iterator();

        while(var2.hasNext()) {
            XNode child = (XNode)var2.next();
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            this.configuration.addInterceptor(interceptorInstance);
        }
    }

}
public void addInterceptor(Interceptor interceptor) {
    this.interceptorChain.addInterceptor(interceptor);
}

当我们获取sqlsession时,sqlsession中包含了Executor,在该方法中会生成Executor的代理对象,当通过Executor执行相应sql时就会被代理进行拦截。 我们查看获取sqlSession的方法:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? this.defaultExecutorType : executorType;
    Object executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }

    if (this.cacheEnabled) {
        executor = new CachingExecutor((Executor)executor);
    }

    return (Executor)this.interceptorChain.pluginAll(executor);
}

在该方法中最后会调用拦截链的pluginAll方法并且返回Executor方法:

public Object pluginAll(Object target) {
    Interceptor interceptor;
    for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
        interceptor = (Interceptor)var2.next();
    }

    return target;
}

该方法会调用拦截器的plugin方法并且返回代理Executor

public static Object wrap(Object target, Interceptor interceptor) {
//获取拦截器上的@Intercepts注解并且根据其type类型为key,method以及args为value放入map中
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //获取target类
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //通过JDK动态代理返回target代理
    return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = (Intercepts)interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) {
        throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    } else {
        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap();
        Signature[] var4 = sigs;
        int var5 = sigs.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Signature sig = var4[var6];
            Set methods = (Set)MapUtil.computeIfAbsent(signatureMap, sig.type(), (k) -> {
                return new HashSet();
            });

            try {
                Method method = sig.type().getMethod(sig.method(), sig.args());
                methods.add(method);
            } catch (NoSuchMethodException var10) {
                throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + var10, var10);
            }
        }

        return signatureMap;
    }
}

InvocationHandler传入了new Plugin(target, interceptor, signatureMap),查看这个类:

public class Plugin implements InvocationHandler {
    private final Object target;
    private final Interceptor interceptor;
    private final Map<Class<?>, Set<Method>> signatureMap;

    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
            return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }

该类实现了InvocationHandler并且重写了invoke方法,当调用Executor执行方法时,如果map中含有该方法,就中转到拦截器的intercept方法中,即我们分页拦截器的intercept方法中

分页原理以及分页拦截器实现原理流程总结

整个流程如下:

1、当调用startPage进行分页时,会创建一个Page对象,填充分页属性,并保存到当前线程ThreadLocal,

2、(Spring解析mybatis-config.xml总配置文件,解析其标签,在此过程中会解析Plugin下的Intercept标签,获取拦截器实例,放入拦截器链中,当获取sqlsession时,会调用每一个拦截器方法,该方法会解析拦截器上的@Intercepts注解获取拦截信息放入map,并且创建代理对象传入Plugin,Plugin实现InvocationHandler,并且传入了map,返回Executor代理),当调用mapper业务时,由Executor执行语句,又由于其是代理对象,方法会被中转到Executor的invoke方法,在该方法中,首先判断执行的方法是不是要拦截的方法,如果是的话,就会调用该拦截器的intercept方法。

3、在Page拦截器的intercept方法中首先会查询数据总条数,设置到Page中,如果没有必要进行分页直接返回,进行分页时会拼接原sql,获取返回list数据后,由于Page继承ArrayList,直接将list放入Page对象,并且返回Page对象,我们得到的数据看似是一个List对象,其实是Page对象的父类引用而已(多态)

4、获取分页数据,将得到的List对象转换为Page对象即可