從 PageHelper 學到的不侵入 Signature 的 AOP

從 PageHelper 學到的不侵入 Signature 的 AOP

前言

最近搭新項目框架,以前 Mybatis 的攔截器都是本身寫的,通常是有個 Page 類型作判斷是否增長分頁 sql。可是這樣一樣的業務開放給頁面和 api 可能要寫兩個,一種帶分頁類型 Page 一種不帶分頁。
發現開源項目 PageHelper 不須要侵入方法的 Signature 就能夠作分頁,特此來源碼分析一下。java

P.S: 看後面的源碼分析最好能懂 mybatis 得攔截器分頁插件原理git

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 服務,每一個請求由一個線程處理的原理。利用線程來決定是否進行分頁。github

是否用 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

怎麼保證以後的 sql 不使用分頁呢?

如: 先查出最近一個月註冊的 10 個用戶,再根據這些用戶查出他們全部的訂單。第一次查詢須要分頁,第二次並不須要api

來看看做者怎麼實現的?mybatis

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();
  }

總結邏輯

  1. 將分頁對象放入 ThreadLocal
  2. 根據 ThreadLocal 判斷是否進行分頁
  3. 不管分頁與不分頁都須要清除 ThreadLocal

備註

<dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper</artifactId>
                <version>5.1.8</version>
            </dependency>
相關文章
相關標籤/搜索