解決mybatis分頁(yè)插件PageHelper導(dǎo)致自定義攔截器失效
問(wèn)題背景
在最近的項(xiàng)目開(kāi)發(fā)中遇到一個(gè)需求 需要對(duì)mysql做一些慢查詢、大結(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ì)于查詢無(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類實(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í)該類是一個(gè)攔截器;
- @Signature:指明自定義攔截器需要攔截哪一個(gè)類型,哪一個(gè)方法
| 攔截的類(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這種做了很多的依賴注入。 如果我們的攔截器需要一些變量對(duì)象,而且這個(gè)對(duì)象是支持可配置的。
類似于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類型操作可以正常攔截 query類型查詢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類型方法 由于mybatis的攔截器為責(zé)任鏈模式調(diào)用有一個(gè)傳遞機(jī)制 (第一個(gè)攔截器執(zhí)行完向下一個(gè)攔截器傳遞 具體實(shí)現(xiàn)可以看一下源碼)
update的操作執(zhí)行確實(shí)進(jì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 查詢
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查詢總數(shù)
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//處理查詢總數(shù),返回 true 時(shí)繼續(xù)分頁(yè)查詢,false 時(shí)直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//當(dāng)查詢總數(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 查詢
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 類型的 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類所在包名 -->
<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ì)查詢第一頁(yè), pageNum>pages(超過(guò)總數(shù)時(shí)),會(huì)查詢最后一頁(yè)。默認(rèn)false 時(shí),直接根據(jù)參數(shù)進(jìn)行查詢。-->
<property name="reasonable" value="true"/>
<!-- supportMethodsArguments:支持通過(guò) Mapper 接口參數(shù)來(lái)傳遞分頁(yè)參數(shù),默認(rèn)值false,分頁(yè)插件會(huì)從查詢方法的參數(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)?。?!
pageHelper的依賴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ò)誤依賴:
<dependency>-->
<!--<groupId>com.github.pagehelper</groupId>-->
<!--<artifactId>pagehelper-spring-boot-starter</artifactId>-->
<!--<version>1.2.12</version>-->
</dependency>
正確依賴
<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-08
SpringBoot通過(guò)注解注入Bean的幾種方式解析
這篇文章主要為大家介紹了SpringBoot注入Bean的幾種方式示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-03-03
Java基于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-08
Springboot+Redis實(shí)現(xiàn)API接口限流的示例代碼
本文主要介紹了Springboot+Redis實(shí)現(xiàn)API接口限流的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07
SpringBoot登錄攔截配置詳解(實(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

