一個意外的接口請求,引發(fā)了刨根問底的沖動
某天突然一個意外地請求,前端是很正常的列表請求,引發(fā)了兩個問題:
1、get請求有傳pageNum和pageSize參數(shù),查詢數(shù)據(jù)庫是正常的基礎(chǔ)select查詢,但是沒有寫limit ?,? 分頁,也沒查到分頁配置,mybatis攔截器等,但是實(shí)現(xiàn)了分頁效果
2、請求查詢時傳了分頁,但是永遠(yuǎn)返回第一頁的數(shù)據(jù)。
一怒之下,必須搞清楚來龍去脈
這讓人有點(diǎn)很想了解他怎么實(shí)現(xiàn)的沖動,于是開始打斷點(diǎn)debug,分析了一波源碼。最后兩個問題通過閱讀源碼均解決了,下面描述下兩個問題的排查過程
第一個問題排查,沒有傳入limit,卻實(shí)現(xiàn)了分頁,難道get請求傳入pageNum和pageSize就能自動分頁,太神奇了吧?
先看下代碼結(jié)構(gòu),簡易代碼:
//Controller代碼:
@GetMapping("/community/getRecordByPage")
@EnablePage
public PageResult getRecordByPage(CommunityRecordDO record) {
try {
List<CommunityArSplitRecordDO> list = recordService.getRecordByPage(record);
return getPageResult(list);
} catch (Exception e) {
log.error("xxxxx,MSG:{}", e.getMessage(), e);
return null;
}
}
//Service代碼:
public List<CommunityRecordDO> getRecordByPage(CommunityRecordDO record) {
return recordMapper.getRecordByPage(record);
}
//mapper
List<CommunityRecordDO> getRecordByPage(CommunityRecordDO record);
//xml
<select id="getRecordByPage" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from t_community_ar_split_record
order by create_time desc
</select>
//請求地址
localhost:8080/api/community/getRecordByPage?pageNum=1&pageSize=10
可以看得出,代碼結(jié)構(gòu)非常簡單,就普通的列表查詢,重點(diǎn):沒有寫limit ?,? 但是他卻實(shí)現(xiàn)了分頁,給前端返回了分頁內(nèi)容。
排查步驟:
1、項目是springboot,查看了下是否有分頁的bean配置,全局搜了,并沒有。
2、查看了整個項目的pom.xml文件,有引入pagehelper,這會算是基本找到方向了(問題剛剛開始)
<!-- pagehelper 分頁插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.spring.boot.starter.version}</version>
</dependency>
3、有引入插件,那應(yīng)該需要配置mybatis的攔截器到Spring的bean容器里面???以前做SpringMVC的是后均是要配置的,但是全局找項目又沒找到,后面想了一下Springboot的自動裝配,是不是pagehelper-spring-boot-starter自動裝配幫忙配了,搜了一下,果不其然,確實(shí)是自動配置了(自動裝配果然名不虛傳)

4、不需要配置攔截器的原因找到了,那我們正常使用pagehelper時一般都是要在執(zhí)行sql前寫一段代碼,證明開啟了分頁的,但是這里也沒有寫
//分頁開啟
PageHelper.startPage(pageNum, pageSize);
5、debug執(zhí)行mapper代理的源碼,跟蹤到攔截器這一步,確確實(shí)實(shí)自動加上了limit,很神奇

6、然后再往上一步如何賦值的地方溯源跟蹤,發(fā)現(xiàn)拿到這個page對象時候,確實(shí)是已經(jīng)有默認(rèn)值了,那在什么時候賦值的呢?繼續(xù)往后查

進(jìn)入getLocalPage()方法,實(shí)際就是一個LOCAL_PAGE對象,還要繼續(xù)往前,看什么時候set進(jìn)去的


在set處打斷點(diǎn),然后根據(jù)堆棧往前找,最終找到了原來是源碼包做了個注解切面@EnablePage,每次使用這個注解時候,就會走這個切面,然后給全局調(diào)用PageHelper.startPage(pageNum, pageSize)


7、已經(jīng)找到源頭了,那@EnablePage在什么時候加的,然后pageNum和pageSize是從哪里來的,其實(shí)問題已經(jīng)解決了,Controller那一層確實(shí)有一個注解,這個注解不是spring注解,還有get請求帶上了兩個參數(shù),這兩個參數(shù)就是分頁配置參數(shù),無非就是從請求地址獲取這兩個參數(shù)做一個全局的開始分頁切面
//請求地址
localhost:8080/api/community/getRecordByPage?pageNum=1&pageSize=10
//Controller代碼:
@GetMapping("/community/getRecordByPage")
//分頁注解
@EnablePage
public PageResult getRecordByPage(CommunityRecordDO record) {
try {
List<CommunityArSplitRecordDO> list = recordService.getRecordByPage(record);
return getPageResult(list);
} catch (Exception e) {
log.error("xxxxx,MSG:{}", e.getMessage(), e);
return null;
}
}

到這,第一個問題已經(jīng)解決了,本以為加上pagehelper自動配置,都不需要配置mybatis攔截器,也不需要寫PageHelper.startPage(pageNum, pageSize),界面get請求傳入pageNum和pageSize就可以實(shí)現(xiàn)分頁,實(shí)則并沒有這么牛,框架并不知道你業(yè)務(wù),所以不會做這種全局的配置。
第二個問題,count記錄只有3條,但是傳了pageNum=2,pageSize=10,但是一直返回第一頁的3條數(shù)據(jù),按道理應(yīng)該返回空,因為不存在第二頁
代碼結(jié)構(gòu)不變,跟第一個問題是相同代碼。傳入pageNum=2,pageSize=10,返回第一頁的數(shù)據(jù),即使把pageNum改成3,4,5也是返回第一頁數(shù)據(jù)
//請求第一頁
localhost:8080/api/community/getRecordByPage?pageNum=1&pageSize=10
//請求第二頁
localhost:8080/api/community/getRecordByPage?pageNum=2&pageSize=10
//請求第三頁
localhost:8080/api/community/getRecordByPage?pageNum=3&pageSize=10
排查步驟:
1、本次思路也是按照第一個方法的步驟去debug跟蹤源碼排查,找到page這個對象,查看它的pageNum值是多少
localhost:8080/api/community/getRecordByPage?pageNum=2&pageSize=10

可以看到,切面是實(shí)質(zhì)拿到了pageNum=2,那為什么執(zhí)行時候返回的是第一頁數(shù)據(jù)呢?
2、排查實(shí)際執(zhí)行的sql,debug到getPageSql這個方法,拿到的page對象,pageNum確實(shí)是1,那可以猜測,從切面設(shè)置為2開始,中途肯定有什么方法讓他改變成1

3、到這里代碼太多,不可能一步步走,此時需要監(jiān)聽pageNum這個字段,看它什么時候變化
第一次賦值是2,跳過

第二次賦值也是2,跳過

第三次賦值是1,好家伙,就是他了,順著堆棧往前看,具體是哪里改變的

貼一下執(zhí)行順序,其實(shí)是在分頁插件攔截器的afterCount里面有一個重新賦值的setTotal方法,然后對pageNum做了重新計算,一些列操作,把它計算成了1



4、問題找到了,那為啥會自動計算成1呢?其實(shí)從備注也看出來了,是為了防止分頁不合理
比如:
1、查詢頁數(shù)比實(shí)際頁數(shù)大,防止不合理,會查詢最后一頁
2、傳遞一個負(fù)數(shù)頁數(shù),返回第一頁
總之這是一個為了防止安全場景考慮的自動計算,主要是靠reasonable=true控制的,因為只有這個字段為true,才會走到pageNum = pages,如果為false,則不會重新賦值,像我的案例,那pageNum應(yīng)該還是2,不應(yīng)該是1
問題是找到了,那又想知道reasonable是在什么時候賦值的呢?代碼都看到這了,那就把它看完整把,繼續(xù)監(jiān)聽該字段的變化
此時入?yún)⒁呀?jīng)是true

往上看,入?yún)⑹且粋€全局變量,也為true,默認(rèn)值是false,所以還是有地方改變了


最終找到是獲取配置項的,只有在項目啟動初期賦值

搜了一下yml,確實(shí)有這個值配置

到此,問題是已經(jīng)排查清楚了,是因為在yml配置pagehelper的分頁合理化配置,導(dǎo)致了分頁一直返回第一頁的bug,其實(shí)也不算是bug,應(yīng)該是屬于分頁合理化考慮,防止本來就沒有第二頁數(shù)據(jù),硬是要查第二頁的信息
如果需要正確返回分頁信息,三種方式:
1、不配置該字段,默認(rèn)值就是false
2、配置該字段,設(shè)置為false
3、使用PageHelper.startPage的重載方法,把false傳入

后記
小小的兩個bug花了竟然快2個小時,一點(diǎn)點(diǎn)debug分析,觀察字段變化,反復(fù)請求從頭來,真費(fèi)神費(fèi)腦,但是結(jié)果是快樂的,又通透了一邊`PageHelper`和`mybatis`的源碼流程。
得出最后結(jié)論的那一刻,突然想到使用這個分頁合理化參數(shù),會導(dǎo)致某種死循環(huán)問題,比如代碼寫法:
int pageNum = 1;
int pageSize = 10;
List<String> result = new ArrayList();
while(true){
List<String> tempList = mapper.select(pageNum,pageSize);
if(tempList.isEmpty){
return;
}
result.add(tempList);
pageNum++;
}
有時候我們做批量導(dǎo)出時,某張表數(shù)據(jù)量很大的時候,不可能一次性全讀出來,那么這時候就會分頁取,通過取不到值去結(jié)束循環(huán),結(jié)果分頁到最后一頁再往后時,由于分頁合理化配置,pageNum++繼續(xù)增加,結(jié)果都是最后一頁,那么循環(huán)就無法結(jié)束,導(dǎo)致死循環(huán)。
所以這個參數(shù)個人感覺還是使用默認(rèn)配置false即可,不需要是否合理化,到底合不合理,由業(yè)務(wù)代碼來評估。