淺析pagehelper分頁原理(轉)


之前項目一直使用的是普元框架,最近公司項目搭建了新框架,主要是由公司的大佬搭建的,以springboot為基礎。為了多學習點東西,我也模仿他搭了一套自己的框架,但是在完成分頁功能的時候,確遇到了問題。

框架的分頁組件使用的是pagehelper,對其我也是早有耳聞,但是也是第一次接觸(ps:工作1年,一直使用的是普元封裝好的前端框架)。

要是用pagehelper,首先maven項目,要引入

<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper</artifactId>
   <version>4.1.6</version>
</dependency>

 

 

前端使用的bootstrap分頁插件,這里不再贅述,直接切入正題,持久層框架使用的是mybatis,分頁插件的URL,指向后台的controller,

@ResponseBody
@RequestMapping("/testPage")
public String testPage(HttpServletRequest request) {
Page<PmProduct> page = PageUtil.pageStart(request);
List<PmProduct> list = prodMapper.selectAll();
JSONObject rst = PageUtil.pageEnd(request, page, list);
return rst.toString();
}
這是controller的代碼,當時就產生一個疑問,為啥在這個pageStart后面查詢就能夠實現分頁呢?可以查出想要的10條分頁數據,而去掉則是全部查詢出來的84條記錄。

帶着問題,我開始了我的debug之旅,首先,我跟進pageStart方法,這個PageUtil類是公司大佬封裝的一個工具類,代碼如下:

public class PageUtil {

public enum CNT{
total,
res
}

public static <E> Page<E> pageStart(HttpServletRequest request){
return pageStart(request,null);
}

/**
*
* @param request
* @param pageSize 每頁顯示條數
* @param orderBy 寫入 需要排序的 字段名 如: product_id desc
* @return
*/
public static <E> Page<E> pageStart(HttpServletRequest request,String orderBy){

int pageon=getPageon(request);
int pageSize=getpageSize(request);
Page<E> page=PageHelper.startPage(pageon, pageSize);
if(!StringUtils.isEmpty(orderBy)){
PageHelper.orderBy(orderBy);
}

return page;
}

private static int getPageon(HttpServletRequest request){
String pageonStr=request.getParameter("pageon");
int pageon;
if(StringUtils.isEmpty(pageonStr)){
pageon=1;
}else{
pageon=Integer.parseInt(pageonStr);
}
return pageon;
}

private static int getpageSize(HttpServletRequest request){
String pageSizeStr=request.getParameter("pageSize");
int pageSize;
if(StringUtils.isEmpty(pageSizeStr)){
pageSize=1;
}else{
pageSize=Integer.parseInt(pageSizeStr);
}
return pageSize;
}

/**
*
* @param request
* @param page
* @param list
* @param elName 頁面顯示所引用的變量名
*/
public static JSONObject pageEnd(HttpServletRequest request, Page<?> page,List<?> list){
JSONObject rstPage=new JSONObject();
rstPage.put(CNT.total.toString(), page.getTotal());
rstPage.put(CNT.res.toString(), list);
return rstPage;
}


}
可以看到,pageStart有兩個方法重載,進入方法后,獲取了前端頁面傳遞的pageon、pageSize兩個參數,分別表示當前頁面和每頁顯示多少條,然后調用了PageHelper.startPage,接着跟進此方法,發現也是一對方法重載,沒關系,往下看
/**
* 開始分頁
*
* @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 = SqlUtil.getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
SqlUtil.setLocalPage(page);
return page;
}
上面的方法才是真正分頁調用的地方,原來是對傳入參數的賦值,賦給Page這個類,繼續,發現getLocalPage和setLoaclPage這兩個方法,很可疑,跟進,看看他到底做了啥,

public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
}

public static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
哦,有點明白了,原來就是賦值保存到本地線程變量里面,這個ThreadLocal是何方神聖,居然有這么厲害,所以查閱了相關博客,鏈接:https://blog.csdn.net/u013521220/article/details/73604917,個人簡單理解大概意思是這個類有4個方法,set,get,remove,initialValue,可以使每個線程獨立開來,參數互不影響,里面保存當前線程的變量副本。

OK,那這個地方就是保存了當前分頁線程的Page參數的變量。有賦值就有取值,那么在下面的分頁過程中,肯定在哪邊取到了這個threadLocal的page參數。

好,執行完startPage,下面就是執行了mybatis的SQL語句,

<select id="selectAll" resultMap="BaseResultMap">
select SEQ_ID, PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESC, CREATE_TIME, EFFECT_TIME,
EXPIRE_TIME, PRODUCT_STATUS, PROVINCE_CODE, REGION_CODE, CHANGE_TIME, OP_OPERATOR_ID,
PRODUCT_SYSTEM, PRODUCT_CODE
from PM_PRODUCT
</select>
SQL語句很簡單,就是簡單的查詢出PM_PRODUCT的全部記錄,那到底是哪邊做了攔截嗎?帶着這個疑問,我跟進了代碼,

發現進入了mybatis的MapperPoxy這個代理類,

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
最后執行的execute方法,再次跟進,進入MapperMethod這個類的execute方法,

public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
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);
result = sqlSession.selectOne(command.getName(), param);
}
} else if (SqlCommandType.FLUSH == command.getType()) {
result = sqlSession.flushStatements();
} else {
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操作,並且查詢出多條,所以就到了executeForMany這個方法中,后面繼續跟進代碼SqlSessionTemplate,DefaultSqlSession(不再贅述),最后可以看到代碼進入了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)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
這下明白了,interceptor是mybatis的攔截器,而PageHelper這個類就實現了interceptor接口,調用其中的intercept方法。

/**
* Mybatis攔截器方法
*
* @param invocation 攔截器入參
* @return 返回執行結果
* @throws Throwable 拋出異常
*/
public Object intercept(Invocation invocation) throws Throwable {
if (autoRuntimeDialect) {
SqlUtil sqlUtil = getSqlUtil(invocation);
return sqlUtil.processPage(invocation);
} else {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
}
/**
* Mybatis攔截器方法
*
* @param invocation 攔截器入參
* @return 返回執行結果
* @throws Throwable 拋出異常
*/
private Object _processPage(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
Page page = null;
//支持方法參數時,會先嘗試獲取Page
if (supportMethodsArguments) {
page = getPage(args);
}
//分頁信息
RowBounds rowBounds = (RowBounds) args[2];
//支持方法參數時,如果page == null就說明沒有分頁條件,不需要分頁查詢
if ((supportMethodsArguments && page == null)
//當不支持分頁參數時,判斷LocalPage和RowBounds判斷是否需要分頁
|| (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
return invocation.proceed();
} else {
//不支持分頁參數時,page==null,這里需要獲取
if (!supportMethodsArguments && page == null) {
page = getPage(args);
}
return doProcessPage(invocation, page, args);
}
}
最終我在SqlUtil中的_processPage方法中找到了,getPage這句話,getLocalPage就將保存在ThreadLocal中的Page變量取了出來,這下一切一目了然了,

 

跟進代碼,發現進入了doProcessPage方法,通過反射機制,首先查詢出數據總數量,然后進行分頁SQL的拼裝,MappedStatement的getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}

// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}

return boundSql;
}
繼續,跟進代碼,發現,最終分頁的查詢,調到了PageStaticSqlSource類的getPageBoundSql中,

protected BoundSql getPageBoundSql(Object parameterObject) {
String tempSql = sql;
String orderBy = PageHelper.getOrderBy();
if (orderBy != null) {
tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
}
tempSql = localParser.get().getPageSql(tempSql);
return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
}
進入getPageSql這個方法,發現,進入了OracleParser類中(還有很多其他的Parser,適用於不同的數據庫),

public String getPageSql(String sql) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
sqlBuilder.append("select * from ( select tmp_page.*, rownum row_id from ( ");
sqlBuilder.append(sql);
sqlBuilder.append(" ) tmp_page where rownum <= ? ) where row_id > ?");
return sqlBuilder.toString();
}
終於,原來分頁的SQL是在這里拼裝起來的。
總結:PageHelper首先將前端傳遞的參數保存到page這個對象中,接着將page的副本存放入ThreadLoacl中,這樣可以保證分頁的時候,參數互不影響,接着利用了mybatis提供的攔截器,取得ThreadLocal的值,重新拼裝分頁SQL,完成分頁。

PS:DEBUG用的好不好,對看框架源碼很有很大的影響。


————————————————
版權聲明:本文為CSDN博主「王小錘DW3」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_21996541/article/details/79796117


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM