首頁 > 軟體

解決mybatis分頁外掛PageHelper導致自定義攔截器失效

2022-08-17 18:00:26

問題背景

在最近的專案開發中遇到一個需求 需要對mysql做一些慢查詢、大結果集等異常指標進行收集監控,從運維角度並沒有對mysql進行統一的指標蒐集,所以需要通過程式碼層面對指標進行收集,我採用的方法是通過mybatis的Interceptor攔截器進行指標收集在開發中出現了自定義攔截器 對於查詢無法進行攔截的問題幾經周折後終於解決,故進行記錄學習,分享給大家下次遇到少走一些彎路;

mybatis攔截器使用

像springmvc一樣,mybatis也提供了攔截器實現,對Executor、StatementHandler、ResultSetHandler、ParameterHandler提供了攔截器功能。

使用方法:

在使用時我們只需要 implements org.apache.ibatis.plugin.Interceptor類實現 方法頭標註相應註解即可 如下程式碼會對CRUD的操作進行攔截:

@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})
})

註解引數介紹:

  • @Intercepts:標識該類是一個攔截器;
  • @Signature:指明自定義攔截器需要攔截哪一個型別,哪一個方法
攔截的類(type)攔截的方法(method)
Executorupdate, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandlergetParameterObject, setParameters
StatementHandlerprepare, parameterize, batch, update, query
ResultSetHandlerhandleResultSets, handleOutputParameters
  • Executor:提供了增刪改查的介面 攔截執行器的方法.
  • StatementHandler:負責處理Mybatis與JDBC之間Statement的互動 攔截引數的處理.
  • ResultSetHandler:負責處理Statement執行後產生的結果集,生成結果列表 攔截結果集的處理.
  • ParameterHandler:是Mybatis實現Sql入參設定的物件 攔截Sql語法構建的處理。

官方程式碼範例:

@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(); //方法引數
     // do something ...... 方法攔截前執行程式碼塊
     Object result = invocation.proceed();
     // do something .......方法攔截後執行程式碼塊
     return result;
   }
   public Object plugin(Object target) {
     return Plugin.wrap(target, this);
   }
}

setProperties方法

因為mybatis框架本身就是一個可以獨立使用的框架,沒有像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>

方法中獲取引數:properties.getProperty("username");

bug內容:

update型別操作可以正常攔截 query型別查詢sql無法進入自定義攔截器,導致攔截失敗以下為部分原始碼 由於涉及到公司程式碼以下程式碼做了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執行器4引數query方法和update型別方法 由於mybatis的攔截器為責任鏈模式呼叫有一個傳遞機制 (第一個攔截器執行完向下一個攔截器傳遞 具體實現可以看一下原始碼)

update的操作執行確實進了自定義攔截器但是查詢的操作始終進不來後通過追蹤原始碼發現

pagehelper外掛的 PageInterceptor 攔截器 會對 Executor執行器method=query 的4引數方法進行修改轉化為 6引數方法 向下傳遞 導致執行順序在pagehelper後面的攔截器的Executor執行器4引數query方法不會接收到傳遞過來的請求導致攔截器失效

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;
            //由於邏輯關係,只會進入一次
            if (args.length == 4) {
                //4 個引數時
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 個引數時
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();
            List resultList;
            //呼叫方法判斷是否需要進行分頁,如果不需要,直接返回結果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判斷是否需要進行 count 查詢
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查詢總數
                    Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                    //處理查詢總數,返回 true 時繼續分頁查詢,false 時直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //當查詢總數為 0 時,直接返回空的結果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用引數值,不使用分頁外掛處理時,仍然支援預設的記憶體分頁
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            if(dialect != null){
                dialect.afterAll();
            }
        }
    }
    /**
     * Spring bean 方式設定時,如果沒有設定屬性就不會執行下面的 setProperties 方法,就不會初始化
     * <p>
     * 因此這裡會出現 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);
            //自動建立
            if (countMs == null) {
                //根據當前的 ms 建立一個返回值為 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;
        }
    }
}
}

解決方法:

通過上述我們定位到了問題產生的原因 解決起來就簡單多了 有倆個方案如下:

  • 調整攔截器順序 讓自定義攔截器先執行
  • 自定義攔截器query方法也定義為 6引數方法或者不使用Executor.class執行器使用StatementHandler.class執行器也可以實現攔截

解決方案一 調整執行順序

mybatis-config.xml 程式碼

我們的自定義攔截器設定的執行順序是在PageInterceptor這個攔截器前面的(先設定後執行)

<plugins>
    <!-- com.github.pagehelper為PageHelper類所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式設定引數,後面會有所有的引數介紹 -->
        <!-- reasonable:分頁合理化引數,預設值為false。當該引數設定為 true 時,pageNum<=0 時會查詢第一頁, pageNum>pages(超過總數時),會查詢最後一頁。預設false 時,直接根據引數進行查詢。-->
        <property name="reasonable" value="true"/>
        <!-- supportMethodsArguments:支援通過 Mapper 介面引數來傳遞分頁引數,預設值false,分頁外掛會從查詢方法的引數值中,自動根據上面 params 設定的欄位中取值,查詢到合適的值時就會自動分頁。 使用方法可以參考測試程式碼中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest。-->
        <property name="supportMethodsArguments" value="true"/>
        <!-- autoRuntimeDialect:預設值為 false。設定為 true 時,允許在執行時根據多資料來源自動識別對應方言的分頁 (不支援自動選擇sqlserver2012,只能使用sqlserver),用法和注意事項參考下面的場景五-->
        <property name="autoRuntimeDialect" value="true"/>
        <!-- params:為了支援startPage(Object params)方法,增加了該引數來設定引數對映,用於從物件中根據屬性名取值, 可以設定 pageNum,pageSize,count,pageSizeZero,reasonable,不設定對映的用預設值, 預設值為pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。-->
    </plugin>
    <plugin interceptor="com.a.b.common.sql.SQLInterceptor"/>
</plugins>

注意點!!!

pageHelper的依賴jar一定要使用pageHelper原生的jar包 pagehelper-spring-boot-starter jar包 是和spring整合的 PageInterceptor會由spring進行管理 在mybatis載入完後就載入了PageInterceptor 會導致mybatis-config.xml 裡調整攔截器順序失效

錯誤依賴:

<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導致自定義攔截器失效的詳細內容,更多關於mybatis PageHelper攔截器失效的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com