解決mybatis分頁(yè)插件PageHelper導(dǎo)致自定義攔截器失效
問(wèn)題背景
在最近的項(xiàng)目開(kāi)發(fā)中遇到一個(gè)需求 需要對(duì)mysql做一些慢查詢(xún)、大結(jié)果集等異常指標(biāo)進(jìn)行收集監(jiān)控,從運(yùn)維角度并沒(méi)有對(duì)mysql進(jìn)行統(tǒng)一的指標(biāo)搜集,所以需要通過(guò)代碼層面對(duì)指標(biāo)進(jìn)行收集,我采用的方法是通過(guò)mybatis的Interceptor攔截器進(jìn)行指標(biāo)收集在開(kāi)發(fā)中出現(xiàn)了自定義攔截器 對(duì)于查詢(xún)無(wú)法進(jìn)行攔截的問(wèn)題幾經(jīng)周折后終于解決,故進(jìn)行記錄學(xué)習(xí),分享給大家下次遇到少走一些彎路;
mybatis攔截器使用
像springmvc一樣,mybatis也提供了攔截器實(shí)現(xiàn),對(duì)Executor、StatementHandler、ResultSetHandler、ParameterHandler提供了攔截器功能。
使用方法:
在使用時(shí)我們只需要 implements org.apache.ibatis.plugin.Interceptor類(lèi)實(shí)現(xiàn) 方法頭標(biāo)注相應(yīng)注解即可 如下代碼會(huì)對(duì)CRUD的操作進(jìn)行攔截:
@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}) })
注解參數(shù)介紹:
- @Intercepts:標(biāo)識(shí)該類(lèi)是一個(gè)攔截器;
- @Signature:指明自定義攔截器需要攔截哪一個(gè)類(lèi)型,哪一個(gè)方法
攔截的類(lèi)(type) | 攔截的方法(method) |
---|---|
Executor | update, query, flushStatements, commit, rollback,getTransaction, close, isClosed |
ParameterHandler | getParameterObject, setParameters |
StatementHandler | prepare, parameterize, batch, update, query |
ResultSetHandler | handleResultSets, handleOutputParameters |
- Executor:提供了增刪改查的接口 攔截執(zhí)行器的方法.
- StatementHandler:負(fù)責(zé)處理Mybatis與JDBC之間Statement的交互 攔截參數(shù)的處理.
- ResultSetHandler:負(fù)責(zé)處理Statement執(zhí)行后產(chǎn)生的結(jié)果集,生成結(jié)果列表 攔截結(jié)果集的處理.
- ParameterHandler:是Mybatis實(shí)現(xiàn)Sql入?yún)⒃O(shè)置的對(duì)象 攔截Sql語(yǔ)法構(gòu)建的處理。
官方代碼示例:
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class TestInterceptor implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); //被代理對(duì)象 Method method = invocation.getMethod(); //代理方法 Object[] args = invocation.getArgs(); //方法參數(shù) // do something ...... 方法攔截前執(zhí)行代碼塊 Object result = invocation.proceed(); // do something .......方法攔截后執(zhí)行代碼塊 return result; } public Object plugin(Object target) { return Plugin.wrap(target, this); } }
setProperties方法
因?yàn)閙ybatis框架本身就是一個(gè)可以獨(dú)立使用的框架,沒(méi)有像Spring這種做了很多的依賴(lài)注入。 如果我們的攔截器需要一些變量對(duì)象,而且這個(gè)對(duì)象是支持可配置的。
類(lèi)似于Spring中的@Value("${}")從application.properties文件中獲取。
使用方法:
mybatis-config.xml配置:
<plugin interceptor="com.plugin.mybatis.MyInterceptor"> <property name="username" value="xxx"/> <property name="password" value="xxx"/> </plugin>
方法中獲取參數(shù):properties.getProperty("username");
bug內(nèi)容:
update類(lèi)型操作可以正常攔截 query類(lèi)型查詢(xún)sql無(wú)法進(jìn)入自定義攔截器,導(dǎo)致攔截失敗以下為部分源碼 由于涉及到公司代碼以下代碼做了mask的處理
自定義攔截器部分代碼
@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 SQLInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { ..... } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { ..... } }
自定義攔截器攔截的是Executor執(zhí)行器4參數(shù)query方法和update類(lèi)型方法 由于mybatis的攔截器為責(zé)任鏈模式調(diào)用有一個(gè)傳遞機(jī)制 (第一個(gè)攔截器執(zhí)行完向下一個(gè)攔截器傳遞 具體實(shí)現(xiàn)可以看一下源碼)
update的操作執(zhí)行確實(shí)進(jìn)了自定義攔截器但是查詢(xún)的操作始終進(jìn)不來(lái)后通過(guò)追蹤源碼發(fā)現(xiàn)
pagehelper插件的 PageInterceptor 攔截器 會(huì)對(duì) Executor執(zhí)行器method=query 的4參數(shù)方法進(jìn)行修改轉(zhuǎn)化為 6參數(shù)方法 向下傳遞 導(dǎo)致執(zhí)行順序在pagehelper后面的攔截器的Executor執(zhí)行器4參數(shù)query方法不會(huì)接收到傳遞過(guò)來(lái)的請(qǐng)求導(dǎo)致攔截器失效
PageInterceptor源碼:
/** * Mybatis - 通用分頁(yè)攔截器 * <p> * GitHub: https://github.com/pagehelper/Mybatis-PageHelper * <p> * Gitee : https://gitee.com/free/Mybatis_PageHelper * * @author liuzh/abel533/isea533 * @version 5.0.0 */ @SuppressWarnings({"rawtypes", "unchecked"}) @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 { private volatile Dialect dialect; private String countSuffix = "_COUNT"; protected Cache<String, MappedStatement> msCountMap = null; private String default_dialect_class = "com.github.pagehelper.PageHelper"; @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; //由于邏輯關(guān)系,只會(huì)進(jìn)入一次 if (args.length == 4) { //4 個(gè)參數(shù)時(shí) boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { //6 個(gè)參數(shù)時(shí) cacheKey = (CacheKey) args[4]; boundSql = (BoundSql) args[5]; } checkDialectExists(); List resultList; //調(diào)用方法判斷是否需要進(jìn)行分頁(yè),如果不需要,直接返回結(jié)果 if (!dialect.skip(ms, parameter, rowBounds)) { //判斷是否需要進(jìn)行 count 查詢(xún) if (dialect.beforeCount(ms, parameter, rowBounds)) { //查詢(xún)總數(shù) Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql); //處理查詢(xún)總數(shù),返回 true 時(shí)繼續(xù)分頁(yè)查詢(xún),false 時(shí)直接返回 if (!dialect.afterCount(count, parameter, rowBounds)) { //當(dāng)查詢(xún)總數(shù)為 0 時(shí),直接返回空的結(jié)果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { //rowBounds用參數(shù)值,不使用分頁(yè)插件處理時(shí),仍然支持默認(rèn)的內(nèi)存分頁(yè) resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { if(dialect != null){ dialect.afterAll(); } } } /** * Spring bean 方式配置時(shí),如果沒(méi)有配置屬性就不會(huì)執(zhí)行下面的 setProperties 方法,就不會(huì)初始化 * <p> * 因此這里會(huì)出現(xiàn) null 的情況 fixed #26 */ private void checkDialectExists() { if (dialect == null) { synchronized (default_dialect_class) { if (dialect == null) { setProperties(new Properties()); } } } } private Long count(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { String countMsId = ms.getId() + countSuffix; Long count; //先判斷是否存在手寫(xiě)的 count 查詢(xún) MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId); if (countMs != null) { count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler); } else { countMs = msCountMap.get(countMsId); //自動(dòng)創(chuàng)建 if (countMs == null) { //根據(jù)當(dāng)前的 ms 創(chuàng)建一個(gè)返回值為 Long 類(lèi)型的 ms countMs = MSUtils.newCountMappedStatement(ms, countMsId); msCountMap.put(countMsId, countMs); } count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler); } return count; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { //緩存 count ms msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties); String dialectClass = properties.getProperty("dialect"); if (StringUtil.isEmpty(dialectClass)) { dialectClass = default_dialect_class; } try { Class<?> aClass = Class.forName(dialectClass); dialect = (Dialect) aClass.newInstance(); } catch (Exception e) { throw new PageException(e); } dialect.setProperties(properties); String countSuffix = properties.getProperty("countSuffix"); if (StringUtil.isNotEmpty(countSuffix)) { this.countSuffix = countSuffix; } } } }
解決方法:
通過(guò)上述我們定位到了問(wèn)題產(chǎn)生的原因 解決起來(lái)就簡(jiǎn)單多了 有倆個(gè)方案如下:
- 調(diào)整攔截器順序 讓自定義攔截器先執(zhí)行
- 自定義攔截器query方法也定義為 6參數(shù)方法或者不使用Executor.class執(zhí)行器使用StatementHandler.class執(zhí)行器也可以實(shí)現(xiàn)攔截
解決方案一 調(diào)整執(zhí)行順序
mybatis-config.xml 代碼
我們的自定義攔截器配置的執(zhí)行順序是在PageInterceptor這個(gè)攔截器前面的(先配置后執(zhí)行)
<plugins> <!-- com.github.pagehelper為PageHelper類(lèi)所在包名 --> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 使用下面的方式配置參數(shù),后面會(huì)有所有的參數(shù)介紹 --> <!-- reasonable:分頁(yè)合理化參數(shù),默認(rèn)值為false。當(dāng)該參數(shù)設(shè)置為 true 時(shí),pageNum<=0 時(shí)會(huì)查詢(xún)第一頁(yè), pageNum>pages(超過(guò)總數(shù)時(shí)),會(huì)查詢(xún)最后一頁(yè)。默認(rèn)false 時(shí),直接根據(jù)參數(shù)進(jìn)行查詢(xún)。--> <property name="reasonable" value="true"/> <!-- supportMethodsArguments:支持通過(guò) Mapper 接口參數(shù)來(lái)傳遞分頁(yè)參數(shù),默認(rèn)值false,分頁(yè)插件會(huì)從查詢(xún)方法的參數(shù)值中,自動(dòng)根據(jù)上面 params 配置的字段中取值,查找到合適的值時(shí)就會(huì)自動(dòng)分頁(yè)。 使用方法可以參考測(cè)試代碼中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest。--> <property name="supportMethodsArguments" value="true"/> <!-- autoRuntimeDialect:默認(rèn)值為 false。設(shè)置為 true 時(shí),允許在運(yùn)行時(shí)根據(jù)多數(shù)據(jù)源自動(dòng)識(shí)別對(duì)應(yīng)方言的分頁(yè) (不支持自動(dòng)選擇sqlserver2012,只能使用sqlserver),用法和注意事項(xiàng)參考下面的場(chǎng)景五--> <property name="autoRuntimeDialect" value="true"/> <!-- params:為了支持startPage(Object params)方法,增加了該參數(shù)來(lái)配置參數(shù)映射,用于從對(duì)象中根據(jù)屬性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默認(rèn)值, 默認(rèn)值為pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。--> </plugin> <plugin interceptor="com.a.b.common.sql.SQLInterceptor"/> </plugins>
注意點(diǎn)?。。?/p>
pageHelper的依賴(lài)jar一定要使用pageHelper原生的jar包 pagehelper-spring-boot-starter jar包 是和spring集成的 PageInterceptor會(huì)由spring進(jìn)行管理 在mybatis加載完后就加載了PageInterceptor 會(huì)導(dǎo)致mybatis-config.xml 里調(diào)整攔截器順序失效
錯(cuò)誤依賴(lài):
<dependency>--> <!--<groupId>com.github.pagehelper</groupId>--> <!--<artifactId>pagehelper-spring-boot-starter</artifactId>--> <!--<version>1.2.12</version>--> </dependency>
正確依賴(lài)
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.10</version> </dependency>
解決方案二 修改攔截器注解定義
@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, CacheKey.class, BoundSql.class}) })
或者
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = StatementHandler.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,ResultHandler.class}) })
以上就是解決mybatis分頁(yè)插件PageHelper導(dǎo)致自定義攔截器失效的詳細(xì)內(nèi)容,更多關(guān)于mybatis PageHelper攔截器失效的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java中BCryptPasswordEncoder密碼的加密與驗(yàn)證方式
這篇文章主要介紹了java中BCryptPasswordEncoder密碼的加密與驗(yàn)證方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08SpringBoot通過(guò)注解注入Bean的幾種方式解析
這篇文章主要為大家介紹了SpringBoot注入Bean的幾種方式示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-03-03Java基于jeeplus vue實(shí)現(xiàn)簡(jiǎn)單工作流過(guò)程圖解
這篇文章主要介紹了Java基于jeeplus vue實(shí)現(xiàn)簡(jiǎn)單工作流過(guò)程圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04如何動(dòng)態(tài)替換Spring容器中的Bean
這篇文章主要介紹了如何動(dòng)態(tài)替換Spring容器中的Bean,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08Springboot+Redis實(shí)現(xiàn)API接口限流的示例代碼
本文主要介紹了Springboot+Redis實(shí)現(xiàn)API接口限流的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07SpringBoot登錄攔截配置詳解(實(shí)測(cè)可用)
這篇文章主要介紹了SpringBoot登錄攔截配置詳解(實(shí)測(cè)可用),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07如何將Spring Session存儲(chǔ)到Redis中實(shí)現(xiàn)持久化
這篇文章主要介紹了如何將Spring Session存儲(chǔ)到Redis中實(shí)現(xiàn)持久化,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07新手學(xué)習(xí)Java對(duì)Redis簡(jiǎn)單操作
這篇文章主要介紹了新手學(xué)習(xí)Java對(duì)Redis簡(jiǎn)單操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04