Mybatis第三方PageHelper分頁(yè)插件的使用與原理
?用法
?
此時(shí)commentAnalyses為Page對(duì)象(PageHelper插件包內(nèi)定義的)
?
而Page對(duì)象繼承自JDK中的ArrayList,擴(kuò)展并封裝了一些page相關(guān)的字段,如頁(yè)碼,每頁(yè)大小,總記錄數(shù),總頁(yè)數(shù)等。
?原理
我們就加了一行,它是如何幫助我們實(shí)現(xiàn)分頁(yè)的呢?請(qǐng)往下看。
PageHelper.startPage做了什么
我們看這一行PageHelper.startPage(pageIndex, pageSize);
做了什么。這個(gè)類中重載了好多個(gè)startPage方法,最終調(diào)用到如下的一個(gè)方法
?可以看到該方法將分頁(yè)信息作為構(gòu)造器參數(shù)實(shí)例化Page對(duì)象,調(diào)用
SqlUtil.getLocalPage()
獲取一個(gè)舊的Page對(duì)象,最后調(diào)用SqlUtil.setLocalPage(page);
把新創(chuàng)建的Page對(duì)象set進(jìn)去。
我們看看這兩個(gè)方法做了什么。如下,很簡(jiǎn)單,從ThreadLocal中獲取Page對(duì)象,將Page對(duì)象set到ThreadLocal中。知道ThreadLocal作用的不用多說(shuō),不知道的可以理解為用于保存本地變量,并與線程綁定。
?看到這我們暫且將這一行的作用記為,創(chuàng)建并保存Page對(duì)象(分頁(yè)信息)到ThreadLocal中。
Page分頁(yè)信息在哪使用
那么既然保存了,就有使用的地方。
我通過(guò)代碼追蹤的方式定位到被調(diào)用的地方,通過(guò)回溯,發(fā)現(xiàn)是從這個(gè)類com.github.pagehelper.dialect.AbstractDialect
發(fā)起調(diào)用的
點(diǎn)進(jìn)去看了一下,主要是取出Page對(duì)象用于做一些判斷,或保存page相關(guān)的信息。應(yīng)該后續(xù)會(huì)涉及到
攔截器
上述AbstractDialect類中的這些方法再回溯,指向了
com.github.pagehelper.util.SqlUtil#doIntercept
方法,intercep調(diào)用了doIntercep方法,
繼續(xù)往上追蹤來(lái)到了com.github.pagehelper.PageHelper#intercept
,這是Interceptor接口的方法。
然后是org.apache.ibatis.plugin.Plugin#invoke
調(diào)用了com.github.pagehelper.PageHelper#intercept
。
可以看到PageHelper實(shí)現(xiàn)了Interceptor,這個(gè)接口是Mybatis官方提供的,中文意思是攔截器,所以有可能是通過(guò)實(shí)現(xiàn)這個(gè)攔截器做了某些操作來(lái)實(shí)現(xiàn)分頁(yè)的。
插件
通過(guò)代碼追蹤我們看到Interceptor的intercept方法是在Mybatis的一個(gè)org.apache.ibatis.plugin.Plugin
類的invoke方法中調(diào)用的,而這個(gè)Plugin類實(shí)現(xiàn)了JDK的java.lang.reflect.InvocationHandler
接口,這是JDK代理接口。
這個(gè)Plugin中有一個(gè)wrap方法會(huì)返回一個(gè)代理類,所以當(dāng)調(diào)用這個(gè)代理類的方法時(shí)就會(huì)走到上面的invoke方法,就可能會(huì)進(jìn)到攔截器的intercept方法。
所以我們看這個(gè)warp在哪調(diào)的,就知道啥時(shí)候創(chuàng)建這個(gè)代理類。就是在上面的PageHelper中,再貼一下代碼
攔截器鏈
而這個(gè)PageHelper中的plugin方法是實(shí)現(xiàn)自Interceptor攔截器接口,所以會(huì)有一個(gè)地方統(tǒng)一調(diào)這個(gè)方法,往上追溯就會(huì)發(fā)現(xiàn)是在org.apache.ibatis.plugin.InterceptorChain
攔截器鏈中調(diào)用的,如下。
該類有一個(gè)List保存所有攔截器,還有三個(gè)方法,分別是pluginAll用于調(diào)用所有攔截器的plugin方法,addInterceptor添加攔截器,getInterceptors獲取攔截器鏈。
看到這大致明白了它的原理,PageHelper通過(guò)實(shí)現(xiàn)Mybatis的Interceptor接口實(shí)現(xiàn)分頁(yè),Mybatis通過(guò)InterceptorChain調(diào)用所有Interceptor。
加載&調(diào)用攔截器
那么我們看看Mybatis的攔截器是什么時(shí)候添加到攔截器鏈,什么時(shí)候被調(diào)用的。
通過(guò)代碼追溯,發(fā)現(xiàn)在Configuration的addInterceptor方法中調(diào)用添加方法,Configuration.addInterceptor是在XMLConfigBuilder的pluginElement方法中被調(diào)用
?而XMLConfigBuilder是解析XML方式的Mybatis的配置的,顧名思義pluginElement方法是解析XML中plugin相關(guān)的配置節(jié)點(diǎn)的
而我們確實(shí)在XML中配置了plugin
所以我們現(xiàn)在知道了mybatis的攔截器是在Mybatis解析配置文件時(shí),解析plugins節(jié)點(diǎn)時(shí)添加到InterceptorChain中的。
攔截器什么時(shí)候調(diào)用的。我們看InterceptorChain的pluginAll方法在哪調(diào)的,通過(guò)代碼追蹤有如下四個(gè)地方調(diào)用攔截器鏈
@Intercepts注解
而PageHelper這個(gè)攔截器,我們可以發(fā)現(xiàn)這個(gè)類上有一個(gè)@Intercepts注解,這個(gè)注解接收的值為@Signature注解,在Signature注解配置了,Executor.class,query還有四個(gè)class:MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class。
了解Mybatis的插件機(jī)制的就明白了,這一行配置的意思是攔截Executor中的query方法,方法參數(shù)列表類型是MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,就是下面這個(gè)方法。
所以看到這,我們可以斷定InterceptorChain的pluginAll方法在上述調(diào)用點(diǎn)的第四個(gè),也就是org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
,可想而知newExecutor,也就是創(chuàng)建Executor實(shí)例,可以斷定,創(chuàng)建Executor時(shí)通過(guò)PageHelper的plugin方法包裝了Executor,返回的是Executor的代理類。下面會(huì)講到創(chuàng)建動(dòng)態(tài)代理。
通過(guò)PageHelper創(chuàng)建代理對(duì)象
我們?cè)谡蚧仡櫼幌拢绾握{(diào)到PageHelper的。首先進(jìn)入到pluginAll
然后會(huì)調(diào)到PageHelper的plugin方法,內(nèi)部又調(diào)Plugin的warp方法
我們看看Plugin.wrap方法干了啥,代碼如下。代碼跟過(guò)來(lái)我們知道現(xiàn)在的target是Executor,interceptor是PageHelper。首先獲取PageHelper攔截信息,然后篩選target是否是需要攔截的類型,這里會(huì)進(jìn)入if判斷邏輯,返回Executor的代理對(duì)象。
所以這時(shí)創(chuàng)建的Executor實(shí)例是代理對(duì)象,那么就會(huì)在某個(gè)時(shí)候調(diào)用代理的invoke方法(org.apache.ibatis.plugin.Plugin#invoke),invoke調(diào)Interceptor攔截器的intercept方法,從而調(diào)PageHelper的intercept方法執(zhí)行分頁(yè)邏輯
org.apache.ibatis.plugin.Plugin#invoke ==》 com.github.pagehelper.PageHelper#intercept
攔截器的調(diào)用源頭-動(dòng)態(tài)代理
因?yàn)榉祷氐氖荅xecutor的動(dòng)態(tài)代理,所以肯定是調(diào)用Executor的某個(gè)方法時(shí)觸發(fā)進(jìn)到invoke方法,具體在哪調(diào)的不好找。我們通過(guò)打斷點(diǎn)的方式看是從哪進(jìn)invoke方法的,首先斷點(diǎn)打到Plugin的invoke方法內(nèi)
通過(guò)調(diào)用??吹绞?code>org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)調(diào)過(guò)來(lái)的,在invoke方法內(nèi)判斷了目標(biāo)方法是不是我們要攔截的方法,因?yàn)镻ageHelper上注解的也是攔截這個(gè)方法,所以會(huì)進(jìn)入到Plugin的invoke方法的第61行。所以就會(huì)進(jìn)入到PageHelper的intercept方法,執(zhí)行具體的攔截邏輯。
分頁(yè)邏輯
思路就是拼SQL。
通過(guò)代碼跟蹤,最終的分頁(yè)邏輯是在com.github.pagehelper.util.SqlUtil#doIntercept方法中,第162行,獲取分頁(yè)SQL,
調(diào)用
com.github.pagehelper.dialect.AbstractDialect#getPageSql(org.apache.ibatis.mapping.MappedStatement, org.apache.ibatis.mapping.BoundSql, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.cache.CacheKey)
在com.github.pagehelper.dialect.AbstractDialect#getPageSql(org.apache.ibatis.mapping.MappedStatement, org.apache.ibatis.mapping.BoundSql, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.cache.CacheKey)
中調(diào)用com.github.pagehelper.dialect.AbstractDialect#getPageSql(java.lang.String, com.github.pagehelper.Page, org.apache.ibatis.session.RowBounds,org.apache.ibatis.cache.CacheKey)
,是一個(gè)抽象方法,具體實(shí)現(xiàn)有好多種
我們看mysql的,在原始SQL 拼接了" limit ?,?"
?總結(jié)
以上是PageHelper實(shí)現(xiàn)分頁(yè)的原理,總結(jié)一下:
Mybatis在四個(gè)地方留了擴(kuò)展點(diǎn),可以通過(guò)自定義攔截器實(shí)現(xiàn)Interceptor接口的plugin方法,執(zhí)行自定義邏輯,可以通過(guò)該方法對(duì)Executor、ParameterHandler、ResultSetHandler、StatementHandler四個(gè)對(duì)象進(jìn)行增強(qiáng)、擴(kuò)展。
PageHelper實(shí)現(xiàn)了Interceptor接口,它的plugin方法調(diào)用Plugin.wrap方法對(duì)目標(biāo)對(duì)象進(jìn)行包裝,包裝成一個(gè)代理對(duì)象并返回,代理類的實(shí)現(xiàn)就是Plugin自身。
Plugin.wrap方法判斷目標(biāo)對(duì)象是否需要返回代理對(duì)象,判斷依據(jù)是:Interceptor實(shí)現(xiàn)類(這里是PageHelper)上注解標(biāo)注的類是否包含目標(biāo)對(duì)象所屬類。這里PageHelper上注解標(biāo)注參數(shù)為Executor對(duì)象,所以創(chuàng)建Executor對(duì)象會(huì)返回代理對(duì)象。
當(dāng)調(diào)用Executor對(duì)象的方法時(shí)會(huì)進(jìn)入到Plugin.invoke方法。invoke方法會(huì)判斷是否需要走攔截器的intercept方法,判斷方式是取攔截器上的注解標(biāo)注的方法,這里PageHelper標(biāo)注的為executor.query(四個(gè)參數(shù)的那個(gè)),所以調(diào)這個(gè)時(shí)才會(huì)被攔截器攔截,其余方法還用原始對(duì)象調(diào)用。
PageHelper的intercept方法調(diào)用SqlUtil的intercept方法最終調(diào)SqlUtil.doIntercept方法。在這個(gè)方法里會(huì)執(zhí)行count語(yǔ)句,并將結(jié)果放到page對(duì)象里,然后判斷需要分頁(yè),則將分頁(yè)sql拼在原始sql上,然后執(zhí)行。
簡(jiǎn)單來(lái)說(shuō)就是通過(guò)mybatis的攔截器和插件實(shí)現(xiàn)的,PageHelper實(shí)現(xiàn)了Interceptor攔截器接口,并攔截Executor的query方法,在執(zhí)行前PageHelper會(huì)在原始SQL前拼裝分頁(yè)相關(guān)的SQL。
PageHelper支持以下數(shù)據(jù)庫(kù)的分頁(yè):Db2、Hsqldbt 、Informix、MySq、Oracle 、SqlServer2012、SqlServer
mybatis的插件Plugin是通過(guò)JDK動(dòng)態(tài)代理對(duì)目標(biāo)對(duì)象進(jìn)行增強(qiáng)
到此這篇關(guān)于Mybatis第三方PageHelper分頁(yè)插件使用與原理的文章就介紹到這了,更多相關(guān)Mybatis第三方PageHelper分頁(yè)插件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot集成RabbitMQ的方法(死信隊(duì)列)
這篇文章主要介紹了SpringBoot集成RabbitMQ的方法(死信隊(duì)列),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05Java中RabbitMQ延遲隊(duì)列實(shí)現(xiàn)詳解
這篇文章主要介紹了Java中RabbitMQ延遲隊(duì)列實(shí)現(xiàn)詳解,消息過(guò)期后,根據(jù)routing-key的不同,又會(huì)被死信交換機(jī)路由到不同的死信隊(duì)列中,消費(fèi)者只需要監(jiān)聽對(duì)應(yīng)的死信隊(duì)列進(jìn)行消費(fèi)即可,需要的朋友可以參考下2023-09-09java的main方法中調(diào)用spring的service方式
這篇文章主要介紹了在java的main方法中調(diào)用spring的service方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Spring Cache和EhCache實(shí)現(xiàn)緩存管理方式
這篇文章主要介紹了Spring Cache和EhCache實(shí)現(xiàn)緩存管理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06解決SpringBoot jar包中的文件讀取問(wèn)題實(shí)現(xiàn)
這篇文章主要介紹了解決SpringBoot jar包中的文件讀取問(wèn)題實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08