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