由于mybatis原生的分页是在内存里面进行的,导致效率很低,但是我们在生产项目中有很多的分页需求,这个时候PageHelper分页插件就诞生了。PageHelper主要是通过插件拦截链实现的。
我们知道在创建StatementHandler 的时候,我们包装了interceptorChain链
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
//给某个对象添加插件拦截器链
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}那这里如何初始化interceptors呢?
我们知道在解析配置文件的时候有一个解析plugin的方法:pluginElement(root.evalNode("plugins"));
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
//解析每一个插件拦截器
for (XNode child : parent.getChildren()) {
//获取配置信息
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//添加参数
interceptorInstance.setProperties(properties);
//添加插件拦截器到configuration中
configuration.addInterceptor(interceptorInstance);
}
}
}
//添加参数 这里看PageInterceptor
public void setProperties(Properties properties) {
//缓存 count ms
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
//这里如果没有配置dialect 走默认的com.github.pagehelper.PageHelper
dialectClass = default_dialect_class;
}
try {
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
dialect.setProperties(properties);
String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
try {
//反射获取 BoundSql 中的 additionalParameters 属性
additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new PageException(e);
}
}
//添加插件拦截器到configuration中
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}然后我们看一下pagehelper是如何实现的
@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首先我们发现这里必须是实现了Interceptor接口,这样才可以配置在plugin标签里
然后我们看一下interceptor.plugin(target);方法
public Object plugin(Object target) {
//TODO Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化,因此考虑在这个方法中做一次判断和初始化
//TODO https://github.com/pagehelper/Mybatis-PageHelper/issues/26
return Plugin.wrap(target, this);
}
public static Object wrap(Object target, Interceptor interceptor) {
//这里解析注解数据
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
//这里获取当前类是否实现了接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//实现了接口 这里生成代理对象 处理handler为Plugin对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
//这里解析注解数据
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
//获取当前Intercepts注解的信息
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//获取注解配置的Signature值
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
//循环所有的Signature
for (Signature sig : sigs) {
//当前Signature 是否有对应的methods
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
//获取配置的方法信息 包括类和方法
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}这里我们可以看到包装在原始executor上面的是一个代理对象,最终执行的时候是交给Plugin的invoke方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取当前所有配置拦截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//包含当前执行的方法
if (methods != null && methods.contains(method)) {
//这里调用的是当前的拦截interceptor (这里是我们的PageInterceptor)
return interceptor.intercept(new Invocation(target, method, args));
}
//直接执行当前executor的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
//PageInterceptor.intercept
//这里配置了两个方法,4个参数和6个参数的
public Object intercept(Invocation invocation) throws Throwable {
try {
//获取所有的入参
Object[] args = invocation.getArgs();
//第一个参数是MappedStatement
MappedStatement ms = (MappedStatement) args[0];
//第二个入参是方法的参数
Object parameter = args[1];
//第三个参数是分页参数
RowBounds rowBounds = (RowBounds) args[2];
//第四个参数是ResultHandler
ResultHandler resultHandler = (ResultHandler) args[3];
//获取执行器Executor
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if(args.length == 4){
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
List resultList;
//1.调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//反射获取动态参数
String msId = ms.getId();
Configuration configuration = ms.getConfiguration();
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
//2.判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
String countMsId = msId + countSuffix;
Long count;
//先判断是否存在手写的 count 查询
MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);
if(countMs != null){
count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自动创建
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
//处理查询总数
//3.返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
//4.判断是否需要进行分页查询
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分页的缓存 key
CacheKey pageKey = cacheKey;
//处理参数对象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//5.调用方言获取分页 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行分页查询
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
//6.结果处理
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
dialect.afterAll();
}
}
1.调用方法判断是否需要进行分页,这里走的是默认dialect:com.github.pagehelper.PageHelper
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
if(ms.getId().endsWith(MSUtils.COUNT)){
throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
}
//获取分页信息
Page page = pageParams.getPage(parameterObject, rowBounds);
//没有分页信息 跳过分页 不执行分页
if (page == null) {
return true;
} else {
//设置默认的 count 列
if(StringUtil.isEmpty(page.getCountColumn())){
page.setCountColumn(pageParams.getCountColumn());
}
autoDialect.initDelegateDialect(ms);
return false;
}
}
//获取分页信息
public Page getPage(Object parameterObject, RowBounds rowBounds) {
//从当前threadLocal中获取分页信息
Page page = PageHelper.getLocalPage();
//如果当前没有分页信息
if (page == null) {
//这里mybatis的分页数据不为默认值
if (rowBounds != RowBounds.DEFAULT) {
//如果设置了RowBounds参数offset作为PageNum使用
if (offsetAsPageNum) {
//生成分页信息
page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
} else {
//没有开启RowBounds参数offset作为PageNum使用
//根据RowBounds参数生成分页信息
page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
//offsetAsPageNum=false的时候,由于PageNum问题,不能使用reasonable,这里会强制为false
page.setReasonable(false);
}
if(rowBounds instanceof PageRowBounds){
PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
}
} else if(supportMethodsArguments){
//mybatis的分页为默认值 然后开启了接口参数来传递分页参数
try {
//从参数中生成分页信息
page = PageObjectUtil.getPageFromObject(parameterObject, false);
} catch (Exception e) {
return null;
}
}
if(page == null){
return null;
}
//把当前分页信息放到threadLocal中
PageHelper.setLocalPage(page);
}
//分页合理化
if (page.getReasonable() == null) {
page.setReasonable(reasonable);
}
//当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
if (page.getPageSizeZero() == null) {
page.setPageSizeZero(pageSizeZero);
}
return page;
}2.判断是否需要进行 count 查询
public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
//这里autoDialect的Delegate实例 是我们设置的mysql
return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);
}
public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
//获取当前分页参数
Page page = getLocalPage();
//这里判断是否只增加排序 而且需要count查询
return !page.isOrderByOnly() && page.isCount();
}3.处理查询总数
public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
//获取分页信息
Page page = getLocalPage();
//设置sql总数
page.setTotal(count);
if (rowBounds instanceof PageRowBounds) {
((PageRowBounds) rowBounds).setTotal(count);
}
//pageSize < 0 的时候,不执行分页查询
//pageSize = 0 的时候,还需要执行后续查询,但是不会分页
if (page.getPageSize() < 0) {
return false;
}
return count > 0;
}4.是否进行分页查询
public boolean beforePage(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
//获取当前分页信息
Page page = getLocalPage();
//只是排序 或者每页大小>0
if (page.isOrderByOnly() || page.getPageSize() > 0) {
return true;
}
return false;
}5.调用方言获取分页 sql
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
//拿到原始SQL
String sql = boundSql.getSql();
//获取分页信息
Page page = getLocalPage();
//支持 order by
//获取order by信息
String orderBy = page.getOrderBy();
if (StringUtil.isNotEmpty(orderBy)) {
pageKey.update(orderBy);
//这里添加order by语句
sql = OrderByParser.converToOrderBySql(sql, orderBy);
}
//如果只是orderby 直接返回sql
if (page.isOrderByOnly()) {
return sql;
}
//组装分页sql
return getPageSql(sql, page, pageKey);
}
//这里添加order by语句
public static String converToOrderBySql(String sql, String orderBy) {
//解析SQL
Statement stmt = null;
try {
stmt = CCJSqlParserUtil.parse(sql);
Select select = (Select) stmt;
SelectBody selectBody = select.getSelectBody();
//处理body-去最外层order by
List<OrderByElement> orderByElements = extraOrderBy(selectBody);
String defaultOrderBy = PlainSelect.orderByToString(orderByElements);
if (defaultOrderBy.indexOf('?') != -1) {
throw new RuntimeException("原SQL[" + sql + "]中的order by包含参数,因此不能使用OrderBy插件进行修改!");
}
//新的sql
sql = select.toString();
} catch (Throwable e) {
e.printStackTrace();
}
return sql + " order by " + orderBy;
}
//组装分页sql mysql
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
//如果开始下标为0 直接取 n条数据
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ? ");
} else {
//组装开始条数 和获取条数
sqlBuilder.append(" LIMIT ?, ? ");
}
//更新缓存key
pageKey.update(page.getPageSize());
return sqlBuilder.toString();
}
6.结果处理
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
//这个方法即使不分页也会被执行,所以要判断 null
AbstractHelperDialect delegate = autoDialect.getDelegate();
if(delegate != null){
//这里走配置的数据库方言 我们这里是mysql
return delegate.afterPage(pageList, parameterObject, rowBounds);
}
return pageList;
}
//这里走配置的数据库方言 我们这里是mysql
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
//获取分页信息
Page page = getLocalPage();
//如果为空 直接返回当前数据
if (page == null) {
return pageList;
}
//当前数据放到分页对象
page.addAll(pageList);
//没有查询总数量 设置总数量为-1
if (!page.isCount()) {
page.setTotal(-1);
} else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
page.setTotal(pageList.size());
} else if(page.isOrderByOnly()){
//如果只是 orderby 设置总数为 当前数据条数
page.setTotal(pageList.size());
}
return page;
}总结:这里的分页插件主要是通过实现mybatis内部的Interceptor接口,然后通过@Intercepts和@Signature注解在确定当前插件在什么时候,什么方法上执行。
版权声明:本文为jadebai原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。