還在手動寫分頁程式碼?看看 PageHelper 如何實現自動化..
記得之前在參加面試的時候,有個面試官給我提了一個問題:請說一下PageHelper分頁外掛的底層原理。當聽到這個問題的時候既熟悉又陌生,熟悉是因為平時都在使用它,熟的不能再熟了;陌生是因為只停留在用的階段,卻沒有沉下心來仔細研究,以至於手足無措。不知道手機前的你是否能準確地描述出來呢?今天就讓我們來認識一下它吧( 此處附上官網地址: https://pagehelper.github.io/ )
首先我們來說一下如何整合和使用它吧(以 Springboot
為例)
pom.xml中引入依賴
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
application.yml中引入配置
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
引數解釋
1.
helperDialect :
分頁外掛會自動檢測當前的資料庫連結,自動選擇合適的分頁方式。
你可以配置
helperDialect
屬性來指定分頁外掛使用哪種方言。
配置時,可以使用下面的縮寫值:
oracle , mysql , mariadb , sqlite , hsqldb , postgresql , db2 , sqlserver , informix , h2 , sqlserver2012 , derby
特別注意:
使用
SqlServer2012
資料庫時,需要手動指定為
sqlserver2012
,否則會使用
SqlServer2005
的方式進行分頁。
你也可以實現
AbstractHelperDialect
,然後配置該屬性為實現類的全限定名稱即可使用自定義的實現方法。
2.
reasonable :
分頁合理化引數,預設值為
false
。
當該引數設定為
true
時,
pageNum<=0
時會查詢第一頁,
pageNum>pages
(超過總數時),會查詢最後一頁。
預設
false
時,直接根據引數進行查詢。
3.
supportMethodsArguments :
支援通過
Mapper
介面引數來傳遞分頁引數,預設值
false
,分頁外掛會從查詢方法的引數值中,自動根據上面
params
配置的欄位中取值,查詢到合適的值時就會自動分頁。
4.
params :
為了支援
startPage(Object params)
方法,增加了該引數來配置引數對映,用於從物件中根據屬性名取值, 可以配置
pageNum,pageSize,count,pageSizeZero,reasonable
,不配置對映的用預設值, 預設值為
pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero= pageSizeZero
。
其他引數
•
offsetAsPageNum :預設值為 false
,該引數對使用 RowBounds
作為分頁引數時有效。當該引數設定為 true
時,會將 RowBounds
中的 offset
引數當成 pageNum
使用,可以用頁碼和頁面大小兩個引數進行分頁。
•
rowBoundsWithCount :預設值為 false
,該引數對使用 RowBounds
作為分頁引數時有效。當該引數設定為 true
時,使用 RowBounds
分頁會進行 count 查詢。
•
pageSizeZero :預設值為 false
,當該引數設定為 true
時,如果 pageSize=0
或者 RowBounds.limit = 0
就會查詢出全部的結果(相當於沒有執行分頁查詢,但是返回結果仍然是 Page
型別)。
•
autoRuntimeDialect :預設值為 false
。設定為 true
時,允許在執行時根據多資料來源自動識別對應方言的分頁 (不支援自動選擇 sqlserver2012
,只能使用 sqlserver
)
•
closeConn :預設值為 true
。當使用執行時動態資料來源或沒有設定 helperDialect
屬性自動獲取資料庫型別時,會自動獲取一個數據庫連線, 通過該屬性來設定是否關閉獲取的這個連線,預設 true
關閉,設定為 false
後,不會關閉獲取的連線,這個引數的設定要根據自己選擇的資料來源來決定。
使用方法
@PostMapping("/list")
public PageInfo<ProductInfo> list(@RequestBody BasePage basePage){
PageHelper.startPage(basePage.getPageNum(),basePage.getPageSize());
List<ProductInfo> list = productInfoService.list(Wrappers.emptyWrapper());
PageInfo<ProductInfo> productInfoPageInfo = new PageInfo<>(list);
return productInfoPageInfo;
}
返回結果
{
"total": 3,
"list": [
{
"id": 1,
"name": "從你的全世界路過",
"price": 32.0000,
"createDate": "2020-11-21T21:26:12",
"updateDate": "2021-03-27T22:17:39"
},
{
"id": 2,
"name": "喬布斯傳",
"price": 25.0000,
"createDate": "2020-11-21T21:26:42",
"updateDate": "2021-03-27T22:17:42"
}
],
"pageNum": 1,
"pageSize": 2,
"size": 2,
"startRow": 1,
"endRow": 2,
"pages": 2,
"prePage": 0,
"nextPage": 2,
"isFirstPage": true,
"isLastPage": false,
"hasPreviousPage": false,
"hasNextPage": true,
"navigatePages": 8,
"navigatepageNums": [
1,
2
],
"navigateFirstPage": 1,
"navigateLastPage": 2
}
接下來讓我們來看看它是如何實現分頁的
一、先說一個小的知識點: ThreadLocal
ThreadLocal
是什麼?有哪些使用場景?
ThreadLocal
是 Java
提供的用來儲存執行緒中區域性變數的類,執行緒區域性變數是侷限於執行緒內部的變數,屬於執行緒自身所有,不被多個執行緒間共享,通過 get
和 set
方法就可以得到當前執行緒對應的值。
Java
提供 ThreadLocal
類來支援執行緒區域性變數,是一種實現執行緒安全的方式。但是在管理環境下(如 web
伺服器)使用執行緒區域性變數的時候要特別小心,在這種情況下,工作執行緒的生命週期比任何應用變數的生命週期都要長。任何執行緒區域性變數一旦在工作完成後沒有釋放, Java
應用就存在記憶體洩露的風險。
對比
•
Synchronized
是通過執行緒等待,犧牲時間來解決訪問衝突
•
ThreadLocal
是通過每個執行緒單獨一份儲存空間,犧牲空間來解決衝突,並且相比於 Synchronized
, ThreadLocal
具有執行緒隔離的效果,只有在執行緒內才能獲取到對應的值,執行緒外則不能訪問到想要的值。
二、看一下 ThreadLocal
在 PageHelper
中的應用(直接上程式碼)
/**
* 分頁呼叫的最終方法
**/
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;
}
//裡邊最重要的就是Page<E> oldPage = getLocalPage();和setLocalPage(page);方法,他倆是看當前執行緒中的
//ThreadLocal.ThreadLocalMap中是否存在該page物件,若存在直接取出,若不存在則設定一個,我們以第一個為例繼續深入
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
/**
* 獲取 Page 引數
* @return
*/
public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
}
public T get() {
//獲取當前執行緒
Thread t = Thread.currentThread();
//獲取當前執行緒中的ThreadLocalMap
ThreadLocalMap map = getMap(t);//ThreadLocal.ThreadLocalMap threadLocals = null;
if (map != null) {
//getEntry(ThreadLocal<?> key)原始碼在下邊
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//=> t.threadLocals = new ThreadLocalMap(this, firstValue);
}
private Entry getEntry(ThreadLocal<?> key) {
//通過hashCode與length位運算確定出一個索引值i,這個i就是被儲存在table陣列中的位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
總結:我們發現在Thread中維護著型別為 ThreadLocal.ThreadLocalMap
的一個引數 threadLocals
,可以把它看作是一個特殊的 map
,它的 key
是 threadLocal
的 threadLocalHashCode
, value
是我們設定的page資訊,其實它底下維護了一個大小為16的環形的 table
陣列,它的負載因子為2/3,我們的資料就存在這個 table
中的 Entry
物件中。
知識點:
1、這裡之所以設定為 WeakReference
,是因為如果這裡使用普通的key-value形式來定義儲存結構,實質上就會造成節點的生命週期與執行緒強繫結,只要執行緒沒有銷燬,那麼節點在GC分析中一直處於可達狀態,沒辦法被回收,而程式本身也無法判斷是否可以清理節點。弱引用是Java中四檔引用的第三檔,比軟引用更加弱一些,如果一個物件沒有強引用鏈可達,那麼一般活不過下一次GC。當某個 ThreadLocal
已經沒有強引用可達,則隨著它被垃圾回收,在 ThreadLocalMap
裡對應的 Entry
的鍵值會失效,這為 ThreadLocalMap
本身的垃圾清理提供了便利。
2、對於某一 ThreadLocal
來講,他的索引值i是確定的,在不同執行緒之間訪問時訪問的是不同的 table
陣列的同一位置即都為table[i],只不過這個不同執行緒之間的table是獨立的。
3、對於同一執行緒的不同 ThreadLocal
來講,這些 ThreadLocal
例項共享一個 table
陣列,然後每個 ThreadLocal
例項在 table
中的索引i是不同的。
三、PageHelper實際攔截SQL
一說到sql的攔截功能,大家應該會想到 Mybatis
的攔截器吧。 Mybatis
攔截器可以對下面4種物件進行攔截:
• Executor:mybatis的內部執行器,作為排程核心負責呼叫StatementHandler操作資料庫,並把結果集通過ResultSetHandler進行自動對映 • StatementHandler:封裝了JDBC Statement操作,是sql語法的構建器,負責和資料庫進行互動執行sql語句 • ParameterHandler:作為處理sql引數設定的物件,主要實現讀取引數和對PreparedStatement的引數進行賦值 • ResultSetHandler:處理Statement執行完成後返回結果集的介面物件,mybatis通過它把ResultSet集合對映成實體物件
估計你也猜到了, PageHelper
也是用的 mybatis
的攔截器進行分頁的,接下來就讓我們看下程式碼吧:首先進入`PageInterceptor` 攔截器
//只關注關鍵程式碼
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
...
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
...
}
}
public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,RowBounds rowBounds, ResultHandler resultHandler,BoundSql boundSql, CacheKey cacheKey) throws SQLException {
//判斷是否需要進行分頁查詢
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分頁的快取 key
CacheKey pageKey = cacheKey;
//處理引數物件
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
...
}
}
獲取到 ThreadLocal
中的 page
物件
@Override
public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
...
return processPageParameter(ms, paramMap, page, boundSql, pageKey);
}
將分頁資料放進引數中,然後執行分頁的邏輯
這樣我們就可以完成分頁了,如果大家想了解 Mybatis
攔截器的具體使用方法,可以後臺私信阿Q。如果覺得這篇文章對你有所幫助,請幫忙點個在看吧。
- 阿里一面:SQL 優化有哪些技巧?
- 面試全程,我只能嗯嗯嗯,差點被pua
- Kafka 面試系列打通關,你能到第幾關?
- 面試官問:你離職的原因是什麼?如何避坑?
- 卷王之王!
- 聊聊高可用的 11 個關鍵技巧
- 京東二面:MySQL 主從延遲,讀寫分離 7 種解決方案
- 【故障演練】 Redis Cluster叢集,當master宕機,主從切換,客戶端報錯 timed out
- 淘寶超時確認收貨 是 如何實現?
- 【萬字長文】電商系統架構, 常見的 9 個大坑 | 庫存超賣、重複下單、物流單ABA...
- 京東一面:MySQL 主備延遲有哪些坑?主備切換策略
- 阿里二面:外部介面大量超時,把整個系統拖垮,引發雪崩!如何解決?熔斷...
- 鎖記——偏向鎖註定過不好這一生
- 【萬字長文】創業公司就應該技術選型 Spring Cloud Alibaba , 開箱即用
- 25種程式碼壞味道總結 優化示例
- 2021年,幫助了很多夥伴晉級阿里、位元組等一線大廠!
- 衝刺金三銀四,你準備好了嗎!
- 阿里一面:講一講 Spring、SpringMVC、SpringBoot、SpringCloud 之間的關係?
- 頭條一面,老是倒在了演算法這一環,怎麼解...
- 5種微服務註冊中心如何選型?這幾個維度告訴你!