一個「小小」的pagehelper

分頁

如果想要了解一個代碼的底層,最好的方法還是從官網的案例和配置說明開始,自頂向下,才能一路通暢
直接就某個類出發,可能在某個從頂層類就存在的參數的建立過程缺失,可能會讓你丈二摸不着頭腦,連往上都不知道怎麼往上
html

前端分頁前端

一次性請求數據表格中的全部記錄(ajax),而後在前端緩存而且計算count和分頁邏輯,通常前端組件(例如dataTable)會提供分頁動做。java

特色是:簡單,很適合小規模的web平臺;當數據量大的時候會產生性能問題,在查詢和網絡傳輸的時間會很長。mysql

後端分頁git

在ajax請求中指定頁碼(pageNum)和每頁的大小(pageSize),後端查詢出當頁的數據返回,前端只負責渲染。github

特色是:複雜一些;性能瓶頸在MySQL的查詢性能,這個固然能夠調優解決。通常來講,web開發使用的是這種方式。web

咱們說的也是後端分頁。ajax

MySQL對分頁的支持:spring

limit關鍵字的用法是
LIMIT [offset,] rows
offset是相對於首行的偏移量(首行是0),rows是返回條數。

# 每頁10條記錄,取第一頁,返回的是前10條記錄
select * from tableA limit 0,10;
# 每頁10條記錄,取第二頁,返回的是第11條記錄,到第20條記錄,
select * from tableA limit 10,10;

1. 引入分頁插件

引入分頁插件有下面2種方式,推薦使用 Maven 方式。sql

1). 引入 Jar 包

你能夠從下面的地址中下載最新版本的 jar 包

因爲使用了sql 解析工具,你還須要下載 jsqlparser.jar(須要和PageHelper 依賴的版本一致) :

2). 使用 Maven

在 pom.xml 中添加以下依賴:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>最新版本</version>
</dependency>

最新版本號能夠從首頁查看。

2. 配置攔截器插件

特別注意,新版攔截器是 com.github.pagehelper.PageInterceptorcom.github.pagehelper.PageHelper 如今是一個特殊的 dialect 實現類,是分頁插件的默認實現類,提供了和之前相同的用法。

1. 在 MyBatis 配置 xml 中配置攔截器插件

<!-- 
    plugins在配置文件中的位置必須符合要求,不然會報錯,順序以下:
    properties?, settings?, 
    typeAliases?, typeHandlers?, 
    objectFactory?,objectWrapperFactory?, 
    plugins?, 
    environments?, databaseIdProvider?, mappers?
-->
<plugins>
    <!-- com.github.pagehelper爲PageHelper類所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置參數,後面會有全部的參數介紹 -->
        <property name="param1" value="value1"/>
	</plugin>
</plugins>

2. 在 Spring 配置文件中配置攔截器插件

使用 spring 的屬性配置方式,可使用 plugins 屬性像下面這樣配置:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <!-- 注意其餘配置 -->
  <property name="plugins">
    <array>
      <bean class="com.github.pagehelper.PageInterceptor">
        <property name="properties">
          <!--使用下面的方式配置參數,一行配置一個 -->
          <value>
            params=value1
          </value>
        </property>
      </bean>
    </array>
  </property>
</bean>

3. 分頁插件參數介紹

分頁插件提供了多個可選參數,這些參數使用時,按照上面兩種配置方式中的示例配置便可。

分頁插件可選參數以下:

  • dialect:默認狀況下會使用 PageHelper 方式進行分頁,若是想要實現本身的分頁邏輯,能夠實現 Dialect(com.github.pagehelper.Dialect) 接口,而後配置該屬性爲實現類的全限定名稱。

下面幾個參數都是針對默認 dialect 狀況下的參數。使用自定義 dialect 實現時,下面的參數沒有任何做用。

  1. helperDialect:分頁插件會自動檢測當前的數據庫連接,自動選擇合適的分頁方式。 你能夠配置helperDialect屬性來指定分頁插件使用哪一種方言。配置時,可使用下面的縮寫值:
    oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby
    特別注意:使用 SqlServer2012 數據庫時,須要手動指定爲 sqlserver2012,不然會使用 SqlServer2005 的方式進行分頁。
    你也能夠實現 AbstractHelperDialect,而後配置該屬性爲實現類的全限定名稱便可使用自定義的實現方法。
  2. offsetAsPageNum:默認值爲 false,該參數對使用 RowBounds 做爲分頁參數時有效。 當該參數設置爲 true 時,會將 RowBounds 中的 offset 參數當成 pageNum 使用,能夠用頁碼和頁面大小兩個參數進行分頁。
  3. rowBoundsWithCount:默認值爲false,該參數對使用 RowBounds 做爲分頁參數時有效。 當該參數設置爲true時,使用 RowBounds 分頁會進行 count 查詢。
  4. pageSizeZero:默認值爲 false,當該參數設置爲 true 時,若是 pageSize=0 或者 RowBounds.limit = 0 就會查詢出所有的結果(至關於沒有執行分頁查詢,可是返回結果仍然是 Page 類型)。
  5. reasonable:分頁合理化參數,默認值爲false。當該參數設置爲 true 時,pageNum<=0 時會查詢第一頁, pageNum>pages(超過總數時),會查詢最後一頁。默認false 時,直接根據參數進行查詢。
  6. params:爲了支持startPage(Object params)方法,增長了該參數來配置參數映射,用於從對象中根據屬性名取值, 能夠配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默認值, 默認值爲pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
  7. supportMethodsArguments:支持經過 Mapper 接口參數來傳遞分頁參數,默認值false,分頁插件會從查詢方法的參數值中,自動根據上面 params 配置的字段中取值,查找到合適的值時就會自動分頁。 使用方法能夠參考測試代碼中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTestArgumentsObjTest
  8. autoRuntimeDialect:默認值爲 false。設置爲 true 時,容許在運行時根據多數據源自動識別對應方言的分頁 (不支持自動選擇sqlserver2012,只能使用sqlserver),用法和注意事項參考下面的場景五
  9. closeConn:默認值爲 true。當使用運行時動態數據源或沒有設置 helperDialect 屬性自動獲取數據庫類型時,會自動獲取一個數據庫鏈接, 經過該屬性來設置是否關閉獲取的這個鏈接,默認true關閉,設置爲 false 後,不會關閉獲取的鏈接,這個參數的設置要根據本身選擇的數據源來決定。
  10. aggregateFunctions(5.1.5+):默認爲全部常見數據庫的聚合函數,容許手動添加聚合函數(影響行數),全部以聚合函數開頭的函數,在進行 count 轉換時,會套一層。其餘函數和列會被替換爲 count(0),其中count列能夠本身配置。

重要提示:

offsetAsPageNum=false 的時候,因爲 PageNum 問題,RowBounds查詢的時候 reasonable 會強制爲 false。使用 PageHelper.startPage 方法不受影響。

4. 如何選擇配置這些參數

單獨看每一個參數的說明多是一件讓人不爽的事情,這裏列舉一些可能會用到某些參數的狀況。

場景一

若是你仍然在用相似ibatis式的命名空間調用方式,你也許會用到rowBoundsWithCount, 分頁插件對RowBounds支持和 MyBatis 默認的方式是一致,默認狀況下不會進行 count 查詢,若是你想在分頁查詢時進行 count 查詢, 以及使用更強大的 PageInfo 類,你須要設置該參數爲 true

注: PageRowBounds 想要查詢總數也須要配置該屬性爲 true

場景二

若是你仍然在用相似ibatis式的命名空間調用方式,你以爲 RowBounds 中的兩個參數 offset,limit 不如 pageNum,pageSize 容易理解, 你可使用 offsetAsPageNum 參數,將該參數設置爲 true 後,offset會當成 pageNum 使用,limitpageSize 含義相同。

場景三

若是以爲某個地方使用分頁後,你仍然想經過控制參數查詢所有的結果,你能夠配置 pageSizeZerotrue, 配置後,當 pageSize=0 或者 RowBounds.limit = 0 就會查詢出所有的結果。

場景四

若是你分頁插件使用於相似分頁查看列表式的數據,如新聞列表,軟件列表, 你但願用戶輸入的頁數不在合法範圍(第一頁到最後一頁以外)時可以正確的響應到正確的結果頁面, 那麼你能夠配置 reasonabletrue,這時若是 pageNum<=0 會查詢第一頁,若是 pageNum>總頁數 會查詢最後一頁。

場景五

若是你在 Spring 中配置了動態數據源,而且鏈接不一樣類型的數據庫,這時你能夠配置 autoRuntimeDialecttrue,這樣在使用不一樣數據源時,會使用匹配的分頁進行查詢。 這種狀況下,你還須要特別注意 closeConn 參數,因爲獲取數據源類型會獲取一個數據庫鏈接,因此須要經過這個參數來控制獲取鏈接後,是否關閉該鏈接。 默認爲 true,有些數據庫鏈接關閉後就無法進行後續的數據庫操做。而有些數據庫鏈接不關閉就會很快因爲鏈接數用完而致使數據庫無響應。因此在使用該功能時,特別須要注意你使用的數據源是否須要關閉數據庫鏈接。

當不使用動態數據源而只是自動獲取 helperDialect 時,數據庫鏈接只會獲取一次,因此不須要擔憂佔用的這一個鏈接是否會致使數據庫出錯,可是最好也根據數據源的特性選擇是否關閉鏈接。

3. 如何在代碼中使用

閱讀前請注意看重要提示

分頁插件支持如下幾種調用方式:

//第一種,RowBounds方式的調用
List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));

//第二種,Mapper接口方式的調用,推薦這種使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);

//第三種,Mapper接口方式的調用,推薦這種使用方式。
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);

//第四種,參數方法調用
//存在如下 Mapper 接口方法,你不須要在 xml 處理後兩個參數
public interface CountryMapper {
    List<User> selectByPageNumSize(
            @Param("user") User user,
            @Param("pageNum") int pageNum, 
            @Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代碼中直接調用:
List<User> list = userMapper.selectByPageNumSize(user, 1, 10);

//第五種,參數對象
//若是 pageNum 和 pageSize 存在於 User 對象中,只要參數有值,也會被分頁
//有以下 User 對象
public class User {
    //其餘fields
    //下面兩個參數名和 params 配置的名字一致
    private Integer pageNum;
    private Integer pageSize;
}
//存在如下 Mapper 接口方法,你不須要在 xml 處理後兩個參數
public interface CountryMapper {
    List<User> selectByPageNumSize(User user);
}
//當 user 中的 pageNum!= null && pageSize!= null 時,會自動分頁
List<User> list = userMapper.selectByPageNumSize(user);

//第六種,ISelect 接口方式
//jdk6,7用法,建立接口
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
    @Override
    public void doSelect() {
        userMapper.selectGroupBy();
    }
});
//jdk8 lambda用法
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy());

//也能夠直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
    @Override
    public void doSelect() {
        userMapper.selectGroupBy();
    }
});
//對應的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> userMapper.selectGroupBy());

//count查詢,返回一個查詢語句的count數
long total = PageHelper.count(new ISelect() {
    @Override
    public void doSelect() {
        userMapper.selectLike(user);
    }
});
//lambda
total = PageHelper.count(()->userMapper.selectLike(user));

下面對最經常使用的方式進行詳細介紹

1). RowBounds方式的調用

List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(1, 10));

使用這種調用方式時,你可使用RowBounds參數進行分頁,這種方式侵入性最小,咱們能夠看到,經過RowBounds方式調用只是使用了這個參數,並無增長其餘任何內容。

分頁插件檢測到使用了RowBounds參數時,就會對該查詢進行物理分頁

關於這種方式的調用,有兩個特殊的參數是針對 RowBounds 的,你能夠參看上面的 場景一場景二

注:不僅有命名空間方式能夠用RowBounds,使用接口的時候也能夠增長RowBounds參數,例如:

//這種狀況下也會進行物理分頁查詢
List<User> selectAll(RowBounds rowBounds);

注意: 因爲默認狀況下的 RowBounds 沒法獲取查詢總數,分頁插件提供了一個繼承自 RowBoundsPageRowBounds,這個對象中增長了 total 屬性,執行分頁查詢後,能夠從該屬性獲得查詢總數。

2). PageHelper.startPage 靜態方法調用

除了 PageHelper.startPage 方法外,還提供了相似用法的 PageHelper.offsetPage 方法。

在你須要進行分頁的 MyBatis 查詢方法前調用 PageHelper.startPage 靜態方法便可,緊跟在這個方法後的第一個MyBatis 查詢方法會被進行分頁。

例一:
//獲取第1頁,10條內容,默認查詢總數count
PageHelper.startPage(1, 10);
//緊跟着的第一個select方法會被分頁
List<User> list = userMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分頁時,實際返回的結果list類型是Page<E>,若是想取出分頁信息,須要強制轉換爲Page<E>
assertEquals(182, ((Page) list).getTotal());
例二:
//request: url?pageNum=1&pageSize=10
//支持 ServletRequest,Map,POJO 對象,須要配合 params 參數
PageHelper.startPage(request);
//緊跟着的第一個select方法會被分頁
List<User> list = userMapper.selectIf(1);

//後面的不會被分頁,除非再次調用PageHelper.startPage
List<User> list2 = userMapper.selectIf(null);
//list1
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分頁時,實際返回的結果list類型是Page<E>,若是想取出分頁信息,須要強制轉換爲Page<E>,
//或者使用PageInfo類(下面的例子有介紹)
assertEquals(182, ((Page) list).getTotal());
//list2
assertEquals(1, list2.get(0).getId());
assertEquals(182, list2.size());
例三,使用PageInfo的用法:
//獲取第1頁,10條內容,默認查詢總數count
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectAll();
//用PageInfo對結果進行包裝
PageInfo page = new PageInfo(list);
//測試PageInfo所有屬性
//PageInfo包含了很是全面的分頁屬性
assertEquals(1, page.getPageNum());
assertEquals(10, page.getPageSize());
assertEquals(1, page.getStartRow());
assertEquals(10, page.getEndRow());
assertEquals(183, page.getTotal());
assertEquals(19, page.getPages());
assertEquals(1, page.getFirstPage());
assertEquals(8, page.getLastPage());
assertEquals(true, page.isFirstPage());
assertEquals(false, page.isLastPage());
assertEquals(false, page.isHasPreviousPage());
assertEquals(true, page.isHasNextPage());

3). 使用參數方式

想要使用參數方式,須要配置 supportMethodsArguments 參數爲 true,同時要配置 params 參數。 例以下面的配置:

<plugins>
    <!-- com.github.pagehelper爲PageHelper類所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置參數,後面會有全部的參數介紹 -->
        <property name="supportMethodsArguments" value="true"/>
        <property name="params" value="pageNum=pageNumKey;pageSize=pageSizeKey;"/>
	</plugin>
</plugins>

在 MyBatis 方法中:

List<User> selectByPageNumSize(
        @Param("user") User user,
        @Param("pageNumKey") int pageNum, 
        @Param("pageSizeKey") int pageSize);

當調用這個方法時,因爲同時發現了 pageNumKeypageSizeKey 參數,這個方法就會被分頁。params 提供的幾個參數均可以這樣使用。

除了上面這種方式外,若是 User 對象中包含這兩個參數值,也能夠有下面的方法:

List<User> selectByPageNumSize(User user);

當從 User 中同時發現了 pageNumKeypageSizeKey 參數,這個方法就會被分頁。

注意:pageNumpageSize 兩個屬性同時存在纔會觸發分頁操做,在這個前提下,其餘的分頁參數纔會生效。

3). PageHelper 安全調用

1. 使用 RowBoundsPageRowBounds 參數方式是極其安全的
2. 使用參數方式是極其安全的
3. 使用 ISelect 接口調用是極其安全的

ISelect 接口方式除了能夠保證安全外,還特別實現了將查詢轉換爲單純的 count 查詢方式,這個方法能夠將任意的查詢方法,變成一個 select count(*) 的查詢方法。

4. 何時會致使不安全的分頁?

PageHelper 方法使用了靜態的 ThreadLocal 參數,分頁參數和線程是綁定的。

只要你能夠保證在 PageHelper 方法調用後緊跟 MyBatis 查詢方法,這就是安全的。由於 PageHelperfinally 代碼段中自動清除了 ThreadLocal 存儲的對象。

若是代碼在進入 Executor 前發生異常,就會致使線程不可用,這屬於人爲的 Bug(例如接口方法和 XML 中的不匹配,致使找不到 MappedStatement 時), 這種狀況因爲線程不可用,也不會致使 ThreadLocal 參數被錯誤的使用。

可是若是你寫出下面這樣的代碼,就是不安全的用法:

PageHelper.startPage(1, 10);
List<User> list;
if(param1 != null){
    list = userMapper.selectIf(param1);
} else {
    list = new ArrayList<User>();
}

這種狀況下因爲 param1 存在 null 的狀況,就會致使 PageHelper 生產了一個分頁參數,可是沒有被消費,這個參數就會一直保留在這個線程上。當這個線程再次被使用時,就可能致使不應分頁的方法去消費這個分頁參數,這就產生了莫名其妙的分頁。

上面這個代碼,應該寫成下面這個樣子:

List<User> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = userMapper.selectIf(param1);
} else {
    list = new ArrayList<User>();
}

這種寫法就能保證安全。

若是你對此不放心,你能夠手動清理 ThreadLocal 存儲的分頁參數,能夠像下面這樣使用:

List<User> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    try{
        list = userMapper.selectAll();
    } finally {
        PageHelper.clearPage();
    }
} else {
    list = new ArrayList<User>();
}

這麼寫很很差看,並且沒有必要。

五、夢開始的地方

  • 探究原理咱們就須要從哪開始會與PageHelper開始有關係,首先PageHelper只能用在selsect上,相關性最大的即是getmapper生成的代理類與sqlsession.selectList方法,而getmapper的最終的實現中也有selectList,從複用的角度,二者應該異曲同工。

  • 大體的流程如此:

    img

  • 而defaultSqlsession中有selectCursor和selectList,二者也都用到了RowBounds

public class DefaultSqlSession implements SqlSession {
public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
    Cursor var6;
    try {
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        Cursor<T> cursor = this.executor.queryCursor(ms, this.wrapCollection(parameter), rowBounds);
        this.registerCursor(cursor);
        var6 = cursor;
    } catch (Exception var10) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var10, var10);
    } finally {
        ErrorContext.instance().reset();
    }

    return var6;
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        List var5;
        try {
             
          // 獲取須要執行的statement語句
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            
         //Page能被引用的緣由的開頭   
         //Page能被引用的緣由的開頭
         //夢開始的地方
            var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }

        return var5;
    }
  • 正常流程下,接着即是調用SimpleExecutor(extends BaseExecutor)中的query方法 ,queryFromDatabase方法,doquery方法(這三個都是BaseExecutor的)
public class SimpleExecutor extends BaseExecutor {

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;

    List var9;
    try {
        Configuration configuration = ms.getConfiguration();
        
        // 實例化一個語句處理類,很關鍵
        StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        
        //獲取connection
        stmt = this.prepareStatement(handler, ms.getStatementLog());
        
        //執行語句
        var9 = handler.query(stmt, resultHandler);
    } finally {
        this.closeStatement(stmt);
    }

    return var9;
}
}
  • Configuration中的newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
	//新建一個StatementHandler的實現類
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    
    
    //最最關鍵的一步,將StatementHandler進行動態代理,實現責任鏈中Interceptor對StatementHandler的加強,生成代理類
    StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}
  • InterceptorChain即是責任鏈的實現類了,他存儲了咱們再.xml文件的plugins中的interceptor,在Configutation起初建立的時候便已經同時建立了,Configuraiton自從sqlsessionFatoryBean容器化調用getObjct後,在buildSqlSessionFactory方法建立後,便一直貫穿了基本全部有sqlsession字樣的業務,sqlsession中「最頂」的類了。
public class InterceptorChain {

// 自configuration被建立時也隨之建立並賦值好了
private final List<Interceptor> interceptors = new ArrayList();

 public Object pluginAll(Object target) {
        Interceptor interceptor;
        
        //將interceptors中的interceptor逐個取出,調用plugin方法,用Plugin類生成代理對象
        for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)var2.next();
        }

        return target;
    }
}
  • 咱們可能對interceptor會有不少疑問

https://www.jianshu.com/p/9c1c78604e4e

問題一: 咱們的 Interceptor 是什麼時候被註冊到 ibatis 的, 註冊到哪裏去了

首先回答註冊到哪裏去: configuration.InterceptorChain 中 , 結構爲 : List interceptors =new ArrayList();

xml 聲明 Interceptor 的地方 有兩個:

\1. ibatis 的 config 配置 中配置 , 這個配置文件 咱們通常叫作 mybatis-config.xml

\2. spring 配置數據源的地方配置 sqlSessionFactory(class="org.mybatis.spring.SqlSessionFactoryBean") 時 以property 的方式給sqlSessionFactoryBean 的 plugins 賦值

① 方式聲明的plugin 添加的 configuration的 InterceptorChain 路徑爲: SqlSessionFactoryBean.afterPropertiesSet().buildSqlSessionFactory() ---> XmlConfigBuilder.parse().parseConfiguration(XNode root).pluginElement(root.evalNode("plugins")).configuration.addInterceptor(interceptorInstance)

進而調用 InterceptorChain的addInterceptor 方法添加 到 InterceptorChain 的 List interceptors =new ArrayList(); 中

②方式聲明的plugin 添加到 configuration的InterceptorChain 路徑爲:

SqlSessionFactoryBean.afterPropertiesSet().buildSqlSessionFactory().configuration.addInterceptor(interceptorInstance)

①和②的實現邏輯都從 sqlSessionFactoryBean.afterPropertiesSet().buildSqlSessionFactory() 開始看就好

**問題二: 咱們的Interceptor 是什麼時候被調用的 , 初次被調用時調用了哪一個方法 **

InterceptorChain 除了問題一的 addInterceptor 方法外 還有兩個方法:

public Object pluginAll(Object target)

public List getInterceptors()

下面咱們看一下 pluginAll 的實現:

public Object pluginAll(Object target) {

for (Interceptor interceptor :interceptors) {

target = interceptor.plugin(target);

}

return target;

}

  • 接下去咱們須要瞭解mybatis 攔截器主體結構,經過一個完整的流程來了解什麼是責任鏈,他的做用,他是什麼時候開始便被決定要調用的。

五、mybatis 攔截器主體結構

http://www.javashuo.com/article/p-tonduauv-nc.html

在編寫 mybatis 插件的時候,首先要實現 Interceptor 接口,而後在 mybatis-conf.xml 中添加插件,

<configuration>
  <plugins>
    <plugin interceptor="***.interceptor1"/>
    <plugin interceptor="***.interceptor2"/>
  </plugins>
</configuration>

這裏須要注意的是,添加的插件是有順序的,由於在解析的時候是依次放入 ArrayList 裏面,而調用的時候其順序爲:2 > 1 > target > 1 > 2;(插件的順序可能會影響執行的流程)更加細緻的講解能夠參考 QueryInterceptor 規範 ;

img

而後當插件初始化完成以後,添加插件的流程以下:

img

首先要注意的是,mybatis 插件的攔截目標有四個,Executor、StatementHandler、ParameterHandler、ResultSetHandler:

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, 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;
}

這裏使用的時候都是用動態代理將多個插件用責任鏈的方式添加的,最後返回的是一個代理對象; 其責任鏈的添加過程以下:

public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

最終動態代理生成和調用的過程都在 Plugin 類中:

public static Object wrap(Object target, Interceptor interceptor) {
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 獲取簽名Map
  Class<?> type = target.getClass(); // 攔截目標 (ParameterHandler|ResultSetHandler|StatementHandler|Executor)
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);  // 獲取目標接口
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(  // 生成代理
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

這裏所說的簽名是指在編寫插件的時候,指定的目標接口和方法,例如:

@Intercepts({
  @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    ...
  }
}

這裏就指定了攔截 Executor 的具備相應方法的 update、query 方法;註解的代碼很簡單,你們能夠自行查看;而後經過 getSignatureMap 方法反射取出對應的 Method 對象,在經過 getAllInterfaces 方法判斷,目標對象是否有對應的方法,有就生成代理對象,沒有就直接反對目標對象;

在調用的時候:

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

六、PageHelper 攔截器分析

mybatis 插件咱們平時使用最多的就是分頁插件了,這裏以 PageHelper 爲例,其使用方法能夠查看相應的文檔 如何使用分頁插件,由於官方文檔講解的很詳細了,我這裏就簡單補充分頁插件須要作哪幾件事情;

使用:

PageHelper.startPage(1, 2);
List<User> list = userMapper1.getAll();

PageHelper 還有不少中使用方式,這是最經常使用的一種,他其實就是在 ThreadLocal 中設置了 Page 對象,能取到就表明須要分頁,在分頁完成後在移除,這樣就不會致使其餘方法分頁;(PageHelper 使用的其餘方法,也是圍繞 Page 對象的設置進行的)

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
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 = getLocalPage();
  if (oldPage != null && oldPage.isOrderByOnly()) {
    page.setOrderBy(oldPage.getOrderBy());
  }
  setLocalPage(page);
  return page;
}

主要實現:

@Intercepts({
  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class PageInterceptor implements Interceptor {

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

      List resultList;
      //調用方法判斷是否須要進行分頁,若是不須要,直接返回結果
      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);
      }
      return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
      if(dialect != null){
        dialect.afterAll();
      }
    }
  }
}
  • 首先能夠看到攔截的是 Executor 的兩個 query 方法(這裏的兩個方法具體攔截到哪個受插件順序影響,最終影響到 cacheKey 和 boundSql 的初始化);
  • 而後使用 checkDialectExists 判斷是否支持對應的數據庫;
  • 在分頁以前須要查詢總數,這裏會生成相應的 sql 語句以及對應的 MappedStatement 對象,並緩存;
  • 而後拼接分頁查詢語句,並生成相應的 MappedStatement 對象,同時緩存;
  • 最後查詢,查詢完成後使用 dialect.afterPage 移除 Page對象

7. ExecutorUtil和MySqlDialect

  • 當代理對象被調用時,便會調用Plugin的wrap方法生成的代理對象的invoke方法,其對調用interceptor.intercept,即上面的主要實現,如果用mysql,則其中的dialect即是MySqlDialect,ExecutorUtil.pageQuery中也會調用的方法。
  • 咱們先講下ExecutorUtil.pageQuery,由於是靜態方法,存於JVM的方法區中,可直接調用。
public abstract class ExecutorUtil {

public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException {
	//判斷是否有Page
    if (!dialect.beforePage(ms, parameter, rowBounds)) 
    {
    	//沒有則用RowBounds.DEFAULT執行query
        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
    } else {
    
    	//執行dialect.processParameterObject和getPageSql
        parameter = dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
        String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
        
        //保存page的sql相關信息
        BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
        Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
        Iterator var12 = additionalParameters.keySet().iterator();

        while(var12.hasNext()) {
            String key = (String)var12.next();
            pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
		
		//用pageBoundSql執行代替舊的sql語句執行
        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
    }
}
  • 接着即是MySqlDialect的時間了,能夠看出是繼承於AbstractHelperDialect(提供了beforePage、beforeCount、afterPage、afterCount等經常使用的判斷,結束處理方法,也實現了processParameterObject(對各個數據庫的Dialect的processPageParameter方法調用前的預處理,生成parameter參數放到paramMap,用來生成應用了Page後的BoundSql))
public class MySqlDialect extends AbstractHelperDialect {
    public MySqlDialect() {
    }

    public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        paramMap.put("First_PageHelper", page.getStartRow());
        paramMap.put("Second_PageHelper", page.getPageSize());
        pageKey.update(page.getStartRow());
        pageKey.update(page.getPageSize());
        if (boundSql.getParameterMappings() != null) {
            List<ParameterMapping> newParameterMappings = new ArrayList(boundSql.getParameterMappings());
            if (page.getStartRow() == 0) {
                newParameterMappings.add((new Builder(ms.getConfiguration(), "Second_PageHelper", Integer.class)).build());
            } else {
                newParameterMappings.add((new Builder(ms.getConfiguration(), "First_PageHelper", Integer.class)).build());
                newParameterMappings.add((new Builder(ms.getConfiguration(), "Second_PageHelper", Integer.class)).build());
            }

            MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
            metaObject.setValue("parameterMappings", newParameterMappings);
        }

        return paramMap;
    }

    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 ?, ? ");
        }

        return sqlBuilder.toString();
    }
}
public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
    Page page = this.getLocalPage();
    if (page.isOrderByOnly()) {
        return parameterObject;
    } else {
        Map<String, Object> paramMap = null;
        if (parameterObject == null) {
            paramMap = new HashMap();
        } else if (parameterObject instanceof Map) {
            paramMap = new HashMap();
            paramMap.putAll((Map)parameterObject);
        } else {
            paramMap = new HashMap();
            boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
            MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);
            if (!hasTypeHandler) {
                String[] var9 = metaObject.getGetterNames();
                int var10 = var9.length;

                for(int var11 = 0; var11 < var10; ++var11) {
                    String name = var9[var11];
                    paramMap.put(name, metaObject.getValue(name));
                }
            }

            if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) {
                Iterator var13 = boundSql.getParameterMappings().iterator();

                ParameterMapping parameterMapping;
                String name;
                do {
                    do {
                        do {
                            do {
                                if (!var13.hasNext()) {
                                    return this.processPageParameter(ms, paramMap, page, boundSql, pageKey);
                                }

                                parameterMapping = (ParameterMapping)var13.next();
                                name = parameterMapping.getProperty();
                            } while(name.equals("First_PageHelper"));
                        } while(name.equals("Second_PageHelper"));
                    } while(paramMap.get(name) != null);
                } while(!hasTypeHandler && !parameterMapping.getJavaType().equals(parameterObject.getClass()));

                paramMap.put(name, parameterObject);
            }
        }

        return this.processPageParameter(ms, paramMap, page, boundSql, pageKey);
    }
}
  • 依照Interceptor 的攔截順序依次實現了相應的Intercepte方法後,相關參數改變,條件判斷時便會直接跳過就如jdbc的execute。
  • 最後即是執行轉換後的sql語句的事情了
相關文章
相關標籤/搜索