做java项目离不来PageHelper,简单说pagehelper就是分页的插件,但是都知道分页其实是对我们要执行的sql之外加上一层,所以相对来说分页其实就是项目开发中的公因式,公因式大家都懂的,开发一个包就可以了。那么我们具体看看pageHelper的原理是是什么的。或者它是怎么做的,我们看看源码的根本想法就是获得一点感悟,这些感悟不仅仅对工作有好处,往远的说还或许影响我们看待事物的角度,久而久之会形成我们的价值观。
从源码中我们看到pagehelper主要有几个包和一些类组成,按照分门别类的思想,那么每个包都对应一个相同的点,而最底下的类应该就是一些功能聚合类。
Page page = PageHelper.startPage(1, 10);
一般来说,我们用pagehelper大概都是上述的方式。
在设置当前页和每页大小的时候,我们看到这里从localpag方法中获取;
public staticPagestartPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
//申明一个分页实体
Page page = new Page(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//从当前线程中获取分页变量
Page oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
//将分页变量设置到线程的threadlocal
setLocalPage(page);
return page;
}
//具体的threadLocal
protected static final ThreadLocalLOCAL_PAGE = new ThreadLocal();
protected static boolean DEFAULT_COUNT = true;
public PageMethod() {
}
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
public staticPagegetLocalPage() {
return (Page)LOCAL_PAGE.get();
}
通过上述分析,我们得出的结论就是pageHelper是通过threadlocal来实现分页的,具体就是将分页的参数放到threadlocal中,这是我们设置完毕之后在没有传递参数的原因。除此之外我们还发现如果一次请求中包含多个分页查询时,其实是用的第一个page,并没有重新定义。
如上图所示,pagehelper应该还是支持上述的数据库的。但是并不知道这块是如何整合的。但是作者发现ibatis的接口intercept存在于pagehelper中,我们发现这个接口提供了一个几个方法和一些注解。
该注解是ibatis提供的,type表示拦截的层次,method表示拦截的方法,args表示参数,这是使用了注解,那么在jdk类加载的时候就会获取到。所以只需要判断是否有这个注解就可以完成判断
@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}
)})
方法setProperties是实例化一个具体的数据库分页实体比如mysql,oracle等。设置好之后,在上边的intercept中就会进行使用。
public void setProperties(Properties properties) {
this.msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
//拿到数据库类型
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = this.default_dialect_class;
}
try {
//通过反射拿到对应的pagehelper
Class aClass = Class.forName(dialectClass);
this.dialect = (Dialect)aClass.newInstance();
} catch (Exception var6) {
throw new PageException(var6);
}
//设置一些参数
this.dialect.setProperties(properties);
String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
try {
this.additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
this.additionalParametersField.setAccessible(true);
} catch (NoSuchFieldException var5) {
throw new PageException(var5);
}
}
执行分页,因为在setProperties方法中已经已经实例化pagehelper,那么这里直接取执行。
public Object intercept(Invocation invocation);
在intercept方法中,我们发现最终也是调用用我们获取分页的相关sql并执行了。
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
Page page = this.getLocalPage();
String orderBy = page.getOrderBy();
if (StringUtil.isNotEmpty(orderBy)) {
pageKey.update(orderBy);
sql = OrderByParser.converToOrderBySql(sql, orderBy);
}
return page.isOrderByOnly() ? sql : this.getPageSql(sql, page, pageKey);
}
分析到这里,我们大概明白了pagehelper仅仅是用来想threadlocal中设置分页参数的,而最周的执行是通过intercept接口来执行的,而接口能被扫描到的原因就是pagehelper中添加了@intercepts注解。既然如此,我们有必要研究一ibatis是如何调用pagehelper的这个接口的。
通过代码跟踪,我们发现plugin类调用了intercept方法,而intercept接口的解析则是方法getSignatureMap完成。
private static Map, Set> 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, Set> signatureMap = new HashMap();
Signature[] var4 = sigs;
int var5 = sigs.length;
for(int var6 = 0; var6 < var5; ++var6) {
Signature sig = var4[var6];
Set methods = (Set)signatureMap.computeIfAbsent(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;
}
}
但是方法wrap方法是谁调用的?作者通过跟踪,发现如下所示。那么我们基本可以得出这个类是ibatis的一个变量,那么是否可以说明ibatis中可以随意调用。
warp方法最后又回调到了plugin类,但是传入的确实this,那么意思就是将自己加入进去。
public Object plugin(Object target) {
//传入pagehelperintercept
return Plugin.wrap(target, this);
}
public static Object wrap(Object target, Interceptor interceptor) {
Map, Set> signatureMap = getSignatureMap(interceptor);
Class type = target.getClass();
Class[] interfaces = getAllInterfaces(type, signatureMap);
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
作者通过查阅相关资料,发现跟作者想的一样,这里的interceptorChain是一个私有变量。并且在创建相应的拦截器的时候都会调用。
但是作者好奇的是这里初始的intercept是在哪里设置的,最后通过作者的跟踪,发现是pagehelper的自动配置包做了这件事情。
问题分析到这里好像是清晰了好多,但是问题是ibatis是如何调用上边的intercept方法的,虽然我们知道他们走的是jdk反射提供的invoke方法,但是什么时候会invoke?