使用mybatis時用PageHelper進行分頁,用到了PageInterceptor,藉此瞭解下mybatis的interceptor。Mybatis的版本是3.4.6,MybatisHelper的版本是5.1.3。java
先上一段代碼,以下List-1:mysql
List-1git
@Test public void testPage() { PageHelper.startPage(2, 3); List<Person> all = personMapper.findAll(); PageInfo<Person> personPageInfo = new PageInfo<>(all,3); log.info(all.toString()); int pageNum = personPageInfo.getPageNum(); int pageSize = personPageInfo.getSize(); int pages = personPageInfo.getPages(); log.info("pageNum:"+pageNum+" size:"+ pageSize +" pages:"+ pages); }
List-1中,查詢全部的Person,不過度頁查詢,注意使用PageHelper後,獲得的List類型的all是com.github.pagehelper.Page,它繼承了JDK的ArrayList,以下List-2所示,我在使用時一開始也覺得是JDK的List實現,直到在看PageInfo時出現一些狀況,深刻了解後才發現是PageHelper繼承的ArrayList:github
List-2sql
public class Page<E> extends ArrayList<E> implements Closeable { private static final long serialVersionUID = 1L; private int pageNum; private int pageSize; private int startRow; private int endRow; private long total; private int pages; private boolean count; private Boolean reasonable; private Boolean pageSizeZero; private String countColumn; private String orderBy; private boolean orderByOnly; ......
mybatis的Interceptor以下List-3所示,PageInterceptor實現了這個接口:數據庫
List-3設計模式
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
來看PageInterceptor如何實現intercept接口的,以下List-4緩存
List-4mybatis
@Override public Object intercept(Invocation invocation) throws Throwable { try { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; Executor executor = (Executor) invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; //因爲邏輯關係,只會進入一次 if(args.length == 4){ //4 個參數時 boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { //6 個參數時 cacheKey = (CacheKey) args[4]; boundSql = (BoundSql) args[5]; } List resultList; //調用方法判斷是否須要進行分頁,若是不須要,直接返回結果 if (!dialect.skip(ms, parameter, rowBounds)) { //反射獲取動態參數 String msId = ms.getId(); Configuration configuration = ms.getConfiguration(); Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql); //判斷是否須要進行 count 查詢 if (dialect.beforeCount(ms, parameter, rowBounds)) { String countMsId = msId + countSuffix; Long count; //先判斷是否存在手寫的 count 查詢 MappedStatement countMs = getExistedMappedStatement(configuration, countMsId); if(countMs != null){ count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler); } else { countMs = msCountMap.get(countMsId); //自動建立 if (countMs == null) { //根據當前的 ms 建立一個返回值爲 Long 類型的 ms countMs = MSUtils.newCountMappedStatement(ms, countMsId); msCountMap.put(countMsId, countMs); } count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler); } //處理查詢總數 //返回 true 時繼續分頁查詢,false 時直接返回 if (!dialect.afterCount(count, parameter, rowBounds)) { //當查詢總數爲 0 時,直接返回空的結果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } //判斷是否須要進行分頁查詢 if (dialect.beforePage(ms, parameter, rowBounds)) { //生成分頁的緩存 key CacheKey pageKey = cacheKey; //處理參數對象 parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey); //調用方言獲取分頁 sql String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey); BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter); //設置動態參數 for (String key : additionalParameters.keySet()) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //執行分頁查詢 resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); } else { //不執行分頁的狀況下,也不執行內存分頁 resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } } else { //rowBounds用參數值,不使用分頁插件處理時,仍然支持默認的內存分頁 resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { dialect.afterAll(); } }
咱們來分析下List-4的內容,"if (!dialect.skip(ms, parameter, rowBounds)) {"判斷是否須要進行分頁,這個dialect就是PageHelper,以下List-5,有興趣能夠看下"pageParams.getPage(parameterObject, rowBounds);",app
List-5
public class PageHelper extends PageMethod implements Dialect { private PageParams pageParams; private PageAutoDialect autoDialect; @Override 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; } } ...
List-4中若是不須要分頁,則直接調用executor的query方法。須要分頁狀況下,首先會看是否須要進行count查詢——List-4中的"if (dialect.beforeCount(ms, parameter, rowBounds))",dialect.beforeCount的最終實現是在AbstractHelperDialect的beforeCount方法,以下List-6,getLocalPage()調用PageHelper.getLocalPage(),獲得com.github.pagehelper.Page——isOrderByOnly默認返還false,isCount默認返還true。
List-6
public abstract class AbstractHelperDialect extends AbstractDialect implements Constant { ... @Override public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) { Page page = getLocalPage(); return !page.isOrderByOnly() && page.isCount(); } ...
List-4中,須要count查詢後,判斷手寫的count已存在,不存在則調用mybatis的builder等工具類構造,以後進行count查詢,獲得結果後調用dialect的afterCount方法,實如今AbstractHelperDialect的afterCount方法,以下List-7,getLocalPage()獲取PageInfo中的Page,以後設置total。在分頁的時候,咱們會對PageHelper進行pageSize的設置,因此只要count的結果大於0,就會返還true。
List-7
@Override public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) { Page page = getLocalPage(); page.setTotal(count); if (rowBounds instanceof PageRowBounds) { ((PageRowBounds) rowBounds).setTotal(count); } //pageSize < 0 的時候,不執行分頁查詢 //pageSize = 0 的時候,還須要執行後續查詢,可是不會分頁 if (page.getPageSize() < 0) { return false; } return count > 0; }
List-4中查詢到count,且大於0,則繼續後續,先判斷是否須要進行分頁查詢——List-4中的"dialect.beforePage(ms, parameter, rowBounds)",實現是在AbstractHelperDialect中,這裏再也不深刻這點,只要咱們的pageSize設置大於0,該方法默認是返還true的。
進行分頁查詢,會調用AbstractHelperDialect的getPageSql方法獲取數據庫執行的sql,以mysql爲例子,AbstractHelperDialect在調用子類MySqlDialect的getPageSql方法,以下List-8,會在sql的最後加上limit語句。AbstractHelperDialect的子類有不少種,對應不一樣的數據庫,這裏使用了模板設計模式。以後再將這個sql交給executor執行,達到分頁的操做。有些人說mybatis的分頁查詢插件底層上是所有查出到內存以後進行切分,可是我看到的是經過limit進行分頁,沒有什麼問題的,控制檯打印的sql也是帶有limit的。
List-8
@Override public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); if (page.getStartRow() == 0) { sqlBuilder.append(" LIMIT ? "); } else { sqlBuilder.append(" LIMIT ?, ? "); } pageKey.update(page.getPageSize()); return sqlBuilder.toString(); }
List-4中,分頁查詢到結果後,調用"dialect.afterPage(resultList, parameter, rowBounds)",即AbstractHelperDialect的afterPage方法,以下List-9,從PageHelper中獲得Page,以後將分頁查詢的結果放入到Page中。
List-9
@Override public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) { Page page = getLocalPage(); if (page == null) { return pageList; } page.addAll(pageList); if (!page.isCount()) { page.setTotal(-1); } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) { page.setTotal(pageList.size()); } else if(page.isOrderByOnly()){ page.setTotal(pageList.size()); } return page; }
List-4中最後的finally塊調用dialect.afterAll(),咱們來看下實現,以下的PageHelper的afterAll(),清理各項清理。
List-10
@Override public void afterAll() { //這個方法即便不分頁也會被執行,因此要判斷 null AbstractHelperDialect delegate = autoDialect.getDelegate(); if (delegate != null) { delegate.afterAll(); autoDialect.clearDelegate(); } clearPage(); }
最終,這個PageInterceptor的intercept方法返還的是本身定義的Page。
來看看PageHelper,它的父類PageMethod,以下的List-10,使用ThreadLocal來存儲Page,若是熟悉JDK的ThreadLocal那麼,對其要注意的點,在PageInterceptor的使用時也要注意,這裏再也不深究。
List-10
public abstract class PageMethod { protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); protected static boolean DEFAULT_COUNT = true; /** * 設置 Page 參數 * * @param page */ protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page); } /** * 獲取 Page 參數 * * @return */ public static <T> Page<T> getLocalPage() { return LOCAL_PAGE.get(); } /** * 移除本地變量 */ public static void clearPage() { LOCAL_PAGE.remove(); } ...
何時調用Interceptor呢,來看mybatis的Configuration的幾個方法,這幾個方法最後都調用了interceptorChain,如List-12所示,使用責任鏈模式,調用plugin方法,最後調用Interceptor的intercept方法。
List-11
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } public Executor newExecutor(Transaction transaction) { return newExecutor(transaction, defaultExecutorType); } public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
List-12
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
思考,插件還能夠作其它什麼呢,咱們能夠用來進行sql性能耗時的統計,或者對更新操做統一的加上更新者、時間等。