热爱技术,追求卓越
不断求索,精益求精

秒懂java,MyBatis分页插件PageHelper基于ThreadLocal的实现原理分析

你或许用过mybatis,但你未必用过github上的一个基于mybatis的分页插件PageHelper。项目地址:

https://github.com/pagehelper/Mybatis-PageHelper

小用了一下,感觉还是蛮不错的。使用MyBatis分页插件PageHelper非常简单,代码如下:

//使用方法可参考https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.select(null);

当看到这么简单的两行代码时,顿时勾起了我的好奇心和求知欲。两行看似没有任何关系的代码,怎么就实现分页了呢?

文档里是这样说的:“在你需要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage 静态方法即可,紧跟在这个方法后的第一个MyBatis 查询方法会被进行分页”。

哦,原来如此,如果只是为了使用这个插件,可能看官方文档的说明就够了。

但是知其然还要知其所以然。我们沿着PageHelper.startPage这个静态方法一探究竟,一步一步的深入,来到了com.github.pagehelper.page.PageMethod类里的下面代码:

    /**
     * 开始分页
     *
     * @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;
    }

上面的代码有个比较关键的地方:

setLocalPage(page);

setLocalPage方法是这样的:

/**
 * 设置 Page 参数
 *
 * @param page
 */
protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

哦,LOCAL_PAGE,这是个啥?看看它的定义是什么鬼:

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

终于明白了,是基于ThreadLocal,但是还没完,我们只看到了set的地方,却没有看到remove的地方,com.github.pagehelper.page.PageMethod类里有个clearPage方法:

/**
 * 移除本地变量
 */
public static void clearPage() {
    LOCAL_PAGE.remove();
}

清除本地线程变量的就是这个clearPage方法,我们再看看这个clearPage会在什么地方调用,看到下面的截图,恍然大悟了。

PageInterceptor这个类的名字是不是特别熟悉?如果你自己实现过mybatis分页插件的话,我想你会取相同的名字。我们看看这个类com.github.pagehelper.PageInterceptor的定义:

public class PageInterceptor implements Interceptor

这个类实现了org.apache.ibatis.plugin.Interceptor接口。在com.github.pagehelper.PageInterceptor.intercept(Invocation)方法的最后finally块中调用了afterAll:

finally {
    dialect.afterAll();
}

来看看com.github.pagehelper.PageHelper中afterAll的实现,最后调用了clearPage方法清除ThreadLocal变量:

@Override
public void afterAll() {
    //这个方法即使不分页也会被执行,所以要判断 null
    AbstractHelperDialect delegate = autoDialect.getDelegate();
    if (delegate != null) {
        delegate.afterAll();
        autoDialect.clearDelegate();
    }
    clearPage();
}

总结起来就是,在你要使用分页查询的时候,先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用mybatis提供的拦截器(插件)实现一个com.github.pagehelper.PageInterceptor接口,这个分页拦截器拦截到后会从ThreadLocal中拿到分页的信息,如果有分页信息,这进行分页查询,最后再把ThreadLocal中的东西清除掉。

所以ThreadLocal在使用过程中一定要明白如何使用,什么时候该清除,尤其是在线程池盛行的年代。类似的场景可以参考我前面的一篇文章《使用ThreadLocal和AOP做线程缓存提高性能,缩短API网关响应时间》。

赞(19)
未经允许不得转载:LoveCTO » 秒懂java,MyBatis分页插件PageHelper基于ThreadLocal的实现原理分析

热爱技术 追求卓越 精益求精