原文鏈接 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,完成分頁。