使用的插件有
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
控制器中使用下面語句就能分頁。
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
方法startPage,有什么用呢?
protected void startPage()
{
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
{
String orderBy = pageDomain.getOrderBy();
PageHelper.startPage(pageNum, pageSize, orderBy);
}
}
startPage首先將前台傳遞過來的分頁信息記錄下來。
public static PageDomain getPageDomain()
{
PageDomain pageDomain = new PageDomain();
pageDomain.setPageNum(ServletUtils.getParameterToInt(Constants.PAGE_NUM));
pageDomain.setPageSize(ServletUtils.getParameterToInt(Constants.PAGE_SIZE));
pageDomain.setOrderByColumn(ServletUtils.getParameter(Constants.ORDER_BY_COLUMN));
pageDomain.setIsAsc(ServletUtils.getParameter(Constants.IS_ASC));
return pageDomain;
}
public static PageDomain buildPageRequest()
{
return getPageDomain();
}
那么PageHelper.startPage(pageNum, pageSize, orderBy)是什么呢?
/**
* 開始分頁
*
* @param pageNum 頁碼
* @param pageSize 每頁顯示數量
* @param orderBy 排序
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, String orderBy) {
Page<E> page = startPage(pageNum, pageSize);
page.setOrderBy(orderBy);
return page;
}
···
/**
* 開始分頁
*
* @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;
}
其中getLocalPage()和setLocalPage();很奇怪:
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static boolean DEFAULT_COUNT = true;
/**
* 設置 Page 參數
*
* @param page
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
/**
* 獲取 Page 參數
*
* @return
*/
public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
}
原來setLocalPage是將page保存到本地線程變量里面。那么有賦值,就該有取值,不然就沒什么用了,對吧,但是在哪里用呢?先跳過這里。回到selectUserList。
執行完startPage,就執行了mybatis的sql語句:
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.phonenumber, u.password, u.sex, u.avatar, u.salt, u.status, u.order_num, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.update_time, u.remark
from sys_user u
where u.del_flag = '0'
group by u.user_id
order by u.order_num, u.user_name, d.order_num
</select>
就是這么簡單的sql語句,為甚頁面會分頁呢?難道有地方進行了攔截處理?
debug發現進入com.baomidou.mybatisplus.core.override的MybatisMapperProxy類中
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
if (privateLookupInMethod == null) {
return invokeDefaultMethodJava8(proxy, method, args);
} else {
return invokeDefaultMethodJava9(proxy, method, args);
}
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
最后執行execute方法,接着進入MybatisMapperMethod類中:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
if (IPage.class.isAssignableFrom(method.getReturnType())) {
assert args != null;
IPage<?> page = null;
for (Object arg : args) {
if (arg instanceof IPage) {
page = (IPage) arg;
break;
}
}
assert page != null;
result = executeForIPage(sqlSession, args);
if (result instanceof PageList) {
PageList pageList = (PageList) result;
page.setRecords(pageList.getRecords());
page.setTotal(pageList.getTotal());
result = page;
} else {
List list = (List<Object>) result;
result = page.setRecords(list);
}
} else {
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
執行的查詢多條的select,所以到了 result = executeForMany(sqlSession, args)
然后是SqlSessionTemplate和DefaultSqlSession
最后可以看到代碼進入了org.apache.ibatis.plugin.Plugin這個類的invoke方法中
@Override
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)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
這下明白了,interceptor是mybatis的攔截器,而com.github.pagehelper.PageInterceptor這個類就實現了interceptor接口,調用其中的intercept方法。
@Override
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) {
//4 個參數時
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 個參數時
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
List resultList;
//調用方法判斷是否需要進行分頁,如果不需要,直接返回結果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判斷是否需要進行 count 查詢
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查詢總數
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//處理查詢總數,返回 true 時繼續分頁查詢,false 時直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//當查詢總數為 0 時,直接返回空的結果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用參數值,不使用分頁插件處理時,仍然支持默認的內存分頁
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}
ExecutorUtil.pageQuery()來處理分頁,但是怎么處理的呢?
com.github.pagehelper.util.ExecutorUtil
public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql, CacheKey cacheKey) throws SQLException {
//判斷是否需要進行分頁查詢
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分頁的緩存 key
CacheKey pageKey = cacheKey;
//處理參數對象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
//調用方言獲取分頁 sql
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
//設置動態參數
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//執行分頁查詢
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不執行分頁的情況下,也不執行內存分頁
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
}
getPageSql方法:
com.github.pagehelper.PageHelper
@Override
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
return autoDialect.getDelegate().getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
}
然后調用com.github.pagehelper.dialect.AbstractHelperDialect
@Override
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
Page page = getLocalPage();
//支持 order by
String orderBy = page.getOrderBy();
if (StringUtil.isNotEmpty(orderBy)) {
pageKey.update(orderBy);
sql = OrderByParser.converToOrderBySql(sql, orderBy);
}
if (page.isOrderByOnly()) {
return sql;
}
return getPageSql(sql, page, pageKey);
}
在getPageSql中getLocalPage將ThreadLocal中page變量取出來。
com.github.pagehelper.dialect.helper.MySqlDialect
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ? ");
} else {
sqlBuilder.append(" LIMIT ?, ? ");
}
return sqlBuilder.toString();
}
PageHelper首先將前端傳遞的參數保存到page這個對象中,接着將page的副本存放入ThreadLoacl中,這樣可以保證分頁的時候,參數互不影響,接着利用了mybatis提供的攔截器,取得ThreadLocal的值,重新拼裝分頁SQL,完成分頁