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

問題背景

在最近的項目開發中遇到一個需求 需要對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)
Executor update, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandler getParameterObject, setParameters
StatementHandler prepare, parameterize, batch, update, query
ResultSetHandler handleResultSets, 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攔截器失效的資料請關註WalkonNet其它相關文章!

推薦閱讀: