1. 程式人生 > >分析Mybatis的分頁外掛PageHelper的原始碼

分析Mybatis的分頁外掛PageHelper的原始碼

本次我們分析PageHelper的原始碼,檢視它的執行過程;

1、PageHelper的版本

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

2、首先我們先看在程式碼中怎麼使用的

PageHelper.startPage(1,2);
List<User> users = sqlSession.selectList("UserMapper.queryAllUser");
users.forEach(u -> System.out.println(u));

3、分析PageHelper.startPage(1,2),這個是在設定當前頁和每頁查詢的資料量

PageHelper裡面有一個靜態方法:
public static Page startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, true);
    }
最後呼叫的方法:
    public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page page = new Page(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        SqlUtil.setLocalPage(page);
        return page;
    }
其實就是建立了一個Page物件,設定一些引數,然後把Page物件放入了ThreadLocal物件裡面。
建立物件的時候還計算了開始和結束的行
private Page(int pageNum, int pageSize, int total, Boolean reasonable) {
        super(0);
        if (pageNum == 1 && pageSize == Integer.MAX_VALUE) {
            pageSizeZero = true;
            pageSize = 0;
        }
        this.pageNum = pageNum;
        this.pageSize = pageSize;
        this.total = total;
        calculateStartAndEndRow();// 計算起止行號
        setReasonable(reasonable);
    }
private void calculateStartAndEndRow() {
        this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
        this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
    }
// 將建立的page物件放入了ThreadLocal物件中
SqlUtil.setLocalPage(page); 
public static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

4、PageHelper實現Interceptor介面,在介面上面有一個註解:

@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}));意思也就是說攔截了Executor的query方法,方法的引數是:MappedStatement,Object,RowBounds,ResultHandler。

實現Interceptor介面會重寫3個方法:

/**
     * Mybatis攔截器方法
     *
     * @param invocation 攔截器入參
     * @return 返回執行結果
     * @throws Throwable 丟擲異常
     */
    public Object intercept(Invocation invocation) throws Throwable {
        return sqlUtil.processPage(invocation);
    }

    /**
     * 只攔截Executor
     *
     * @param target
     * @return
     */
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    /**
     * 設定屬性值
     *
     * @param p 屬性值
     */
    public void setProperties(Properties p) {
        //MyBatis3.2.0版本校驗
        try {
            Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0之後新增的類
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("您使用的MyBatis版本太低,MyBatis分頁外掛PageHelper支援MyBatis3.2.0及以上版本!");
        }
        //資料庫方言
        String dialect = p.getProperty("dialect");
        sqlUtil = new SqlUtil(dialect);
        sqlUtil.setProperties(p);
    }

5、我們的分頁邏輯在intercept(Invocation invocation)方法裡面,所以主要來分析這個方法

public Object intercept(Invocation invocation) throws Throwable {
        return sqlUtil.processPage(invocation);
    }
// 呼叫的是這個方法
public Object processPage(Invocation invocation) throws Throwable {
        try {
            Object result = _processPage(invocation);
            return result;// 這裡返回的其實是一個Page物件
        } finally {
            clearLocalPage();
        }
    }
// 接下來
private Object _processPage(Invocation invocation) throws Throwable {
        final Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds) args[2];
        if (SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
            return invocation.proceed();
        } else {
            //忽略RowBounds-否則會進行Mybatis自帶的記憶體分頁
            args[2] = RowBounds.DEFAULT;
            //分頁資訊
            Page page = getPage(rowBounds);
            //pageSizeZero的判斷
            if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
                //執行正常(不分頁)查詢
                Object result = invocation.proceed();
                //得到處理結果
                page.addAll((List) result);
                //相當於查詢第一頁
                page.setPageNum(1);
                //這種情況相當於pageSize=total
                page.setPageSize(page.size());
                //仍然要設定total
                page.setTotal(page.size());
                //返回結果仍然為Page型別 - 便於後面對接收型別的統一處理
                return page;
            }
            //獲取原始的ms
            MappedStatement ms = (MappedStatement) args[0];
            SqlSource sqlSource = ms.getSqlSource();
            //簡單的通過total的值來判斷是否進行count查詢
            if (page.isCount()) {
                //將引數中的MappedStatement替換為新的qs
                msUtils.processCountMappedStatement(ms, sqlSource, args);
                //查詢總數
                Object result = invocation.proceed();
                //設定總數
                page.setTotal((Integer) ((List) result).get(0));
                if (page.getTotal() == 0) {
                    return page;
                }
            }
            //pageSize>0的時候執行分頁查詢,pageSize<=0的時候不執行相當於可能只返回了一個count
            if (page.getPageSize() > 0 &&
                    ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                            || rowBounds != RowBounds.DEFAULT)) {
                //將引數中的MappedStatement替換為新的qs
                msUtils.processPageMappedStatement(ms, sqlSource, page, args);
                //執行分頁查詢
                Object result = invocation.proceed();
                //得到處理結果
                page.addAll((List) result);
            }
            //返回結果
            return page;
        }
    }

6、分頁其實分成兩步:第一步,查詢一共有多少條資料(Count);第二步,查詢當前頁的資料(LIMIT ?,?);我們先分析統計總數的程式碼

# 分析count的原始碼
主要的程式碼如下:
MappedStatement ms = (MappedStatement) args[0];
SqlSource sqlSource = ms.getSqlSource();
 //簡單的通過total的值來判斷是否進行count查詢
 if (page.isCount()) {
       //將引數中的MappedStatement替換為新的qs
       msUtils.processCountMappedStatement(ms, sqlSource, args);
        //查詢總數
       Object result = invocation.proceed();
       //設定總數
      page.setTotal((Integer) ((List) result).get(0));
      if (page.getTotal() == 0) {
               return page;
             }
  }
# 關鍵程式碼:msUtils.processCountMappedStatement(ms, sqlSource, args);//將引數中的MappedStatement替換為新的qs
public void processCountMappedStatement(MappedStatement ms, SqlSource sqlSource, Object[] args) {
        args[0] = getMappedStatement(ms, sqlSource, args[1], SUFFIX_COUNT);
    }

public MappedStatement getMappedStatement(MappedStatement ms, SqlSource sqlSource, Object parameterObject, String suffix) {
        MappedStatement qs = null;
        if (ms.getId().endsWith(SUFFIX_PAGE) || ms.getId().endsWith(SUFFIX_COUNT)) {
            throw new RuntimeException("分頁外掛配置錯誤:請不要在系統中配置多個分頁外掛(使用Spring時,mybatis-config.xml和Spring<bean>配置方式,請選擇其中一種,不要同時配置多個分頁外掛)!");
        }
        if (parser.isSupportedMappedStatementCache()) {
            try {
                qs = ms.getConfiguration().getMappedStatement(ms.getId() + suffix);
            } catch (Exception e) {
                //ignore
            }
        }
        if (qs == null) {
            //建立一個新的MappedStatement
            qs = newMappedStatement(ms, getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT), suffix);
            if (parser.isSupportedMappedStatementCache()) {
                try {
                    ms.getConfiguration().addMappedStatement(qs);
                } catch (Exception e) {
                    //ignore
                }
            }
        }
        return qs;
    }
# 關鍵程式碼:qs = newMappedStatement(ms, getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT), suffix);中的getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT)
public SqlSource getsqlSource(MappedStatement ms, SqlSource sqlSource, Object parameterObject, boolean count) {
        if (sqlSource instanceof DynamicSqlSource) {//動態sql
            MetaObject msObject = SystemMetaObject.forObject(ms);
            SqlNode sqlNode = (SqlNode) msObject.getValue("sqlSource.rootSqlNode");
            MixedSqlNode mixedSqlNode;
            if (sqlNode instanceof MixedSqlNode) {
                mixedSqlNode = (MixedSqlNode) sqlNode;
            } else {
                List<SqlNode> contents = new ArrayList<SqlNode>(1);
                contents.add(sqlNode);
                mixedSqlNode = new MixedSqlNode(contents);
            }
            return new PageDynamicSqlSource(this, ms.getConfiguration(), mixedSqlNode, count);
        } else if (sqlSource instanceof ProviderSqlSource) {//註解式sql
            return new PageProviderSqlSource(parser, ms.getConfiguration(), (ProviderSqlSource) sqlSource, count);
        } else if (count) {//RawSqlSource和StaticSqlSource
            return getStaticCountSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
        } else {
            return getStaticPageSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
        }
    }
# 關鍵程式碼
else if (count) {//RawSqlSource和StaticSqlSource
            return getStaticCountSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
        }
public SqlSource getStaticCountSqlSource(Configuration configuration, SqlSource sqlSource, Object parameterObject) {
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        return new StaticSqlSource(configuration, parser.getCountSql(boundSql.getSql()), boundSql.getParameterMappings());
    }
# 關鍵程式碼:parser.getCountSql(boundSql.getSql())
public String getCountSql(final String sql) {
        return sqlParser.getSmartCountSql(sql);
    }
# 接下來這個方法就是將sql處理為count
public String getSmartCountSql(String sql) {
        //校驗是否支援該sql
        isSupportedSql(sql);
        if (CACHE.get(sql) != null) {
            return CACHE.get(sql);
        }
        //解析SQL
        Statement stmt = null;
        try {
            stmt = CCJSqlParserUtil.parse(sql);
        } catch (Throwable e) {
            //無法解析的用一般方法返回count語句
            String countSql = getSimpleCountSql(sql);
            CACHE.put(sql, countSql);
            return countSql;
        }
        Select select = (Select) stmt;
        SelectBody selectBody = select.getSelectBody();
        //處理body-去order by
        processSelectBody(selectBody);
        //處理with-去order by
        processWithItemsList(select.getWithItemsList());
        //處理為count查詢
        sqlToCount(select);
        String result = select.toString();
        CACHE.put(sql, result);
        return result;
    }
# 關鍵程式碼:sqlToCount(select);
public void sqlToCount(Select select) {
        SelectBody selectBody = select.getSelectBody();
        // 是否能簡化count查詢
        if (selectBody instanceof PlainSelect && isSimpleCount((PlainSelect) selectBody)) {
            ((PlainSelect) selectBody).setSelectItems(COUNT_ITEM);
        } else {
            PlainSelect plainSelect = new PlainSelect();
            SubSelect subSelect = new SubSelect();
            subSelect.setSelectBody(selectBody);
            subSelect.setAlias(TABLE_ALIAS);
            plainSelect.setFromItem(subSelect);
            plainSelect.setSelectItems(COUNT_ITEM);
            select.setSelectBody(plainSelect);
        }
    }
# 最終將原來的sql改成countsql之後返回
Object result = invocation.proceed();# 執行sql完成查詢
page.setTotal((Integer) ((List) result).get(0));// 設定總數
if (page.getTotal() == 0) {
    return page;// 如果count的結果為0,就不進行limit查詢了,直接返回page物件。
}
# 所以count這一步分析完畢了。

7、分析limit查詢的這一步驟

//將引數中的MappedStatement替換為新的qs
msUtils.processPageMappedStatement(ms, sqlSource, page, args);
Object result = invocation.proceed();
//得到處理結果
page.addAll((List) result);

# 關鍵程式碼:msUtils.processPageMappedStatement(ms, sqlSource, page, args);
public void processPageMappedStatement(MappedStatement ms, SqlSource sqlSource, Page page, Object[] args) {
        args[0] = getMappedStatement(ms, sqlSource, args[1], SUFFIX_PAGE);
        //處理入參
        args[1] = setPageParameter((MappedStatement) args[0], args[1], page);
    }
# 先看處理sql,其實就是加上limit ?,?(args[0] = getMappedStatement(ms, sqlSource, args[1], SUFFIX_PAGE);)
public MappedStatement getMappedStatement(MappedStatement ms, SqlSource sqlSource, Object parameterObject, String suffix) {
        MappedStatement qs = null;
        if (ms.getId().endsWith(SUFFIX_PAGE) || ms.getId().endsWith(SUFFIX_COUNT)) {
            throw new RuntimeException("分頁外掛配置錯誤:請不要在系統中配置多個分頁外掛(使用Spring時,mybatis-config.xml和Spring<bean>配置方式,請選擇其中一種,不要同時配置多個分頁外掛)!");
        }
        if (parser.isSupportedMappedStatementCache()) {
            try {
                qs = ms.getConfiguration().getMappedStatement(ms.getId() + suffix);
            } catch (Exception e) {
                //ignore
            }
        }
        if (qs == null) {
            //建立一個新的MappedStatement
            qs = newMappedStatement(ms, getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT), suffix);
            if (parser.isSupportedMappedStatementCache()) {
                try {
                    ms.getConfiguration().addMappedStatement(qs);
                } catch (Exception e) {
                    //ignore
                }
            }
        }
        return qs;
    }
# 關鍵程式碼:getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT)
public SqlSource getsqlSource(MappedStatement ms, SqlSource sqlSource, Object parameterObject, boolean count) {
        if (sqlSource instanceof DynamicSqlSource) {//動態sql
            MetaObject msObject = SystemMetaObject.forObject(ms);
            SqlNode sqlNode = (SqlNode) msObject.getValue("sqlSource.rootSqlNode");
            MixedSqlNode mixedSqlNode;
            if (sqlNode instanceof MixedSqlNode) {
                mixedSqlNode = (MixedSqlNode) sqlNode;
            } else {
                List<SqlNode> contents = new ArrayList<SqlNode>(1);
                contents.add(sqlNode);
                mixedSqlNode = new MixedSqlNode(contents);
            }
            return new PageDynamicSqlSource(this, ms.getConfiguration(), mixedSqlNode, count);
        } else if (sqlSource instanceof ProviderSqlSource) {//註解式sql
            return new PageProviderSqlSource(parser, ms.getConfiguration(), (ProviderSqlSource) sqlSource, count);
        } else if (count) {//RawSqlSource和StaticSqlSource
            return getStaticCountSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
        } else {
            return getStaticPageSqlSource(ms.getConfiguration(), sqlSource, parameterObject);// 這次是走這裡
        }
    }
# 關鍵程式碼:return getStaticPageSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
public SqlSource getStaticPageSqlSource(Configuration configuration, SqlSource sqlSource, Object parameterObject) {
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        return new StaticSqlSource(configuration, parser.getPageSql(boundSql.getSql()), parser.getPageParameterMapping(configuration, boundSql));
    }
# 關鍵程式碼:parser.getPageSql(boundSql.getSql())
public String getPageSql(String sql) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        sqlBuilder.append(" limit ?,?");
        return sqlBuilder.toString();
    }
# 就是在後面加上了" limit ?,?"。最終返回了
# 再看怎麼處理引數
args[1] = setPageParameter((MappedStatement) args[0], args[1], page);
public Map setPageParameter(MappedStatement ms, Object parameterObject, Page page) {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        return parser.setPageParameter(ms, parameterObject, boundSql, page);
    }
public Map setPageParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql, Page page) {
        Map paramMap = super.setPageParameter(ms, parameterObject, boundSql, page);
        paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());// 設定第一個引數
        paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());// 設定第二個引數
        return paramMap;
    }
# 其實就是設定limit後面的引數,兩個引數的分別為:First_PageHelper(值為page物件的startRow),Second_PageHelper(值為page物件的pageSize)
# 返回替換的sql,和新增引數,直接分頁查詢
Object result = invocation.proceed();
# 將結果新增到page中,page繼承了ArrayList
page.addAll((List) result);
最終返回了page物件

8、至此,原始碼分析完畢。注意Page繼承了ArrayList