淺析pagehelper分頁原理
原文連結 https://blog.csdn.net/qq_21996541/article/details/79796117
之前專案一直使用的是普元框架,最近公司專案搭建了新框架,主要是由公司的大佬搭建的,以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,完成分頁。