分页使用
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对象即可