解決mybatis分頁插件PageHelper導(dǎo)致自定義攔截器失效
問題背景
在最近的項(xiàng)目開發(fā)中遇到一個需求 需要對mysql做一些慢查詢、大結(jié)果集等異常指標(biāo)進(jìn)行收集監(jiān)控,從運(yùn)維角度并沒有對mysql進(jìn)行統(tǒng)一的指標(biāo)搜集,所以需要通過代碼層面對指標(biāo)進(jìn)行收集,我采用的方法是通過mybatis的Interceptor攔截器進(jìn)行指標(biāo)收集在開發(fā)中出現(xiàn)了自定義攔截器 對于查詢無法進(jìn)行攔截的問題幾經(jīng)周折后終于解決,故進(jìn)行記錄學(xué)習(xí),分享給大家下次遇到少走一些彎路;
mybatis攔截器使用
像springmvc一樣,mybatis也提供了攔截器實(shí)現(xiàn),對Executor、StatementHandler、ResultSetHandler、ParameterHandler提供了攔截器功能。
使用方法:
在使用時我們只需要 implements org.apache.ibatis.plugin.Interceptor類實(shí)現(xiàn) 方法頭標(biāo)注相應(yīng)注解即可 如下代碼會對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)識該類是一個攔截器;
- @Signature:指明自定義攔截器需要攔截哪一個類型,哪一個方法
| 攔截的類(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è)置的對象 攔截Sql語法構(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(); //被代理對象
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框架本身就是一個可以獨(dú)立使用的框架,沒有像Spring這種做了很多的依賴注入。 如果我們的攔截器需要一些變量對象,而且這個對象是支持可配置的。
類似于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無法進(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)用有一個傳遞機(jī)制 (第一個攔截器執(zhí)行完向下一個攔截器傳遞 具體實(shí)現(xiàn)可以看一下源碼)
update的操作執(zhí)行確實(shí)進(jìn)了自定義攔截器但是查詢的操作始終進(jìn)不來后通過追蹤源碼發(fā)現(xiàn)
pagehelper插件的 PageInterceptor 攔截器 會對 Executor執(zhí)行器method=query 的4參數(shù)方法進(jìn)行修改轉(zhuǎn)化為 6參數(shù)方法 向下傳遞 導(dǎo)致執(zhí)行順序在pagehelper后面的攔截器的Executor執(zhí)行器4參數(shù)query方法不會接收到傳遞過來的請求導(dǎo)致攔截器失效
PageInterceptor源碼:
/**
* Mybatis - 通用分頁攔截器
* <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)系,只會進(jìn)入一次
if (args.length == 4) {
//4 個參數(shù)時
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 個參數(shù)時
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
List resultList;
//調(diào)用方法判斷是否需要進(jìn)行分頁,如果不需要,直接返回結(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 時繼續(xù)分頁查詢,false 時直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//當(dāng)查詢總數(shù)為 0 時,直接返回空的結(jié)果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用參數(shù)值,不使用分頁插件處理時,仍然支持默認(rèn)的內(nèi)存分頁
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}
/**
* Spring bean 方式配置時,如果沒有配置屬性就不會執(zhí)行下面的 setProperties 方法,就不會初始化
* <p>
* 因此這里會出現(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;
//先判斷是否存在手寫的 count 查詢
MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
if (countMs != null) {
count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自動創(chuàng)建
if (countMs == null) {
//根據(jù)當(dāng)前的 ms 創(chuàng)建一個返回值為 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;
}
}
}
}
解決方法:
通過上述我們定位到了問題產(chǎn)生的原因 解決起來就簡單多了 有倆個方案如下:
- 調(diào)整攔截器順序 讓自定義攔截器先執(zhí)行
- 自定義攔截器query方法也定義為 6參數(shù)方法或者不使用Executor.class執(zhí)行器使用StatementHandler.class執(zhí)行器也可以實(shí)現(xiàn)攔截
解決方案一 調(diào)整執(zhí)行順序
mybatis-config.xml 代碼
我們的自定義攔截器配置的執(zhí)行順序是在PageInterceptor這個攔截器前面的(先配置后執(zhí)行)
<plugins>
<!-- com.github.pagehelper為PageHelper類所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置參數(shù),后面會有所有的參數(shù)介紹 -->
<!-- reasonable:分頁合理化參數(shù),默認(rèn)值為false。當(dāng)該參數(shù)設(shè)置為 true 時,pageNum<=0 時會查詢第一頁, pageNum>pages(超過總數(shù)時),會查詢最后一頁。默認(rèn)false 時,直接根據(jù)參數(shù)進(jìn)行查詢。-->
<property name="reasonable" value="true"/>
<!-- supportMethodsArguments:支持通過 Mapper 接口參數(shù)來傳遞分頁參數(shù),默認(rèn)值false,分頁插件會從查詢方法的參數(shù)值中,自動根據(jù)上面 params 配置的字段中取值,查找到合適的值時就會自動分頁。 使用方法可以參考測試代碼中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest。-->
<property name="supportMethodsArguments" value="true"/>
<!-- autoRuntimeDialect:默認(rèn)值為 false。設(shè)置為 true 時,允許在運(yùn)行時根據(jù)多數(shù)據(jù)源自動識別對應(yīng)方言的分頁 (不支持自動選擇sqlserver2012,只能使用sqlserver),用法和注意事項(xiàng)參考下面的場景五-->
<property name="autoRuntimeDialect" value="true"/>
<!-- params:為了支持startPage(Object params)方法,增加了該參數(shù)來配置參數(shù)映射,用于從對象中根據(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會由spring進(jìn)行管理 在mybatis加載完后就加載了PageInterceptor 會導(dǎo)致mybatis-config.xml 里調(diào)整攔截器順序失效
錯誤依賴:
<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分頁插件PageHelper導(dǎo)致自定義攔截器失效的詳細(xì)內(nèi)容,更多關(guān)于mybatis PageHelper攔截器失效的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java中BCryptPasswordEncoder密碼的加密與驗(yàn)證方式
這篇文章主要介紹了java中BCryptPasswordEncoder密碼的加密與驗(yàn)證方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08
Java基于jeeplus vue實(shí)現(xiàn)簡單工作流過程圖解
這篇文章主要介紹了Java基于jeeplus vue實(shí)現(xiàn)簡單工作流過程圖解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-04-04
Springboot+Redis實(shí)現(xiàn)API接口限流的示例代碼
本文主要介紹了Springboot+Redis實(shí)現(xiàn)API接口限流的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-07-07
如何將Spring Session存儲到Redis中實(shí)現(xiàn)持久化
這篇文章主要介紹了如何將Spring Session存儲到Redis中實(shí)現(xiàn)持久化,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07

