從 PageHelper 學到的不侵入 Signature 的 AOP
從 PageHelper 學到的不侵入 Signature 的 AOP
前言
最近搭新專案框架,之前 Mybatis 的攔截器都是自己寫的,一般是有個 Page 型別做判斷是否增加分頁 sql。但是這樣同樣的業務開放給頁面和 api 可能要寫兩個,一種帶分頁型別 Page 一種不帶分頁。
發現開源專案 PageHelper 不需要侵入方法的 Signature 就可以做分頁,特此來原始碼分析一下。
P.S:
看後面的原始碼分析最好能懂 mybatis 得攔截器分頁外掛原理
PageHelper 的簡答使用
public PageInfo<RpmDetail> listRpmDetailByCondition(String filename, Date startTime, Date endTime, Integer categoryId, Integer ostypeId, Integer statusId, Integer pageNo, Integer pageSize) { PageHelper.startPage(pageNo, pageSize); List<RpmDetail> result = rpmDetailMapper.listRpmDetailByCondition(filename, startTime, endTime, categoryId, ostypeId, statusId); return new PageInfo(result); }
PageHelper.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; }
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal(); protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page); }
可以看出在真正使用分頁前,生成了一個 page 物件,然後放入了 ThreadLocal 中,這個思想很巧妙,利用每個請求 Web 服務,每個請求由一個執行緒處理的原理。利用執行緒來決定是否進行分頁。
是否用 ThreadLocal 來判斷是否分頁的猜想?
//呼叫方法判斷是否需要進行分頁,如果不需要,直接返回結果 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); }
@Override private PageParams pageParams; public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) { if (ms.getId().endsWith(MSUtils.COUNT)) { throw new RuntimeException("在系統中發現了多個分頁外掛,請檢查系統配置!"); } Page page = pageParams.getPage(parameterObject, rowBounds); if (page == null) { return true; } else { //設定預設的 count 列 if (StringUtil.isEmpty(page.getCountColumn())) { page.setCountColumn(pageParams.getCountColumn()); } autoDialect.initDelegateDialect(ms); return false; } }
/** * 獲取分頁引數 * * @param parameterObject * @param rowBounds * @return */ public Page getPage(Object parameterObject, RowBounds rowBounds) { Page page = PageHelper.getLocalPage(); if (page == null) { if (rowBounds != RowBounds.DEFAULT) { if (offsetAsPageNum) { page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount); } else { page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount); //offsetAsPageNum=false的時候,由於PageNum問題,不能使用reasonable,這裡會強制為false page.setReasonable(false); } if(rowBounds instanceof PageRowBounds){ PageRowBounds pageRowBounds = (PageRowBounds)rowBounds; page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount()); } } else if(parameterObject instanceof IPage || supportMethodsArguments){ try { page = PageObjectUtil.getPageFromObject(parameterObject, false); } catch (Exception e) { return null; } } if(page == null){ return null; } PageHelper.setLocalPage(page); } //分頁合理化 if (page.getReasonable() == null) { page.setReasonable(reasonable); } //當設定為true的時候,如果pagesize設定為0(或RowBounds的limit=0),就不執行分頁,返回全部結果 if (page.getPageSizeZero() == null) { page.setPageSizeZero(pageSizeZero); } return page; }
/** * 獲取 Page 引數 * * @return */ public static <T> Page<T> getLocalPage() { return LOCAL_PAGE.get(); }
果然如此,至此真相已經揭開。
怎麼保證之後的 sql 不使用分頁呢?
如:
先查出最近一個月註冊的 10 個使用者,再根據這些使用者查出他們所有的訂單。第一次查詢需要分頁,第二次並不需要
來看看作者怎麼實現的?
try{ # 分頁攔截邏輯 } finally { dialect.afterAll(); }
@Override public void afterAll() { //這個方法即使不分頁也會被執行,所以要判斷 null AbstractHelperDialect delegate = autoDialect.getDelegate(); if (delegate != null) { delegate.afterAll(); autoDialect.clearDelegate(); } clearPage(); }
/** * 移除本地變數 */ public static void clearPage() { LOCAL_PAGE.remove(); }
總結邏輯
- 將分頁物件放入 ThreadLocal
- 根據 ThreadLocal 判斷是否進行分頁
- 無論分頁與不分頁都需要清除 ThreadLocal
備註
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.8</version> </dependency>