自定義Mybatis-plus外掛(限制最大查詢數量)

2023-04-19 15:00:38

自定義Mybatis-plus外掛(限制最大查詢數量)

需求背景

​ 一次查詢如果結果返回太多(1萬或更多),往往會導致系統效能下降,有時更會記憶體不足,影響系統穩定性,故需要做限制。

解決思路

1.經分析最後決定,應限制一次查詢返回的最大結果數量不應該超出1萬,對於一次返回結果大於限制的時候應該丟擲異常,而不應該擷取(limit 10000)最大結果(結果需求不匹配)。

2.利用mybatis攔截器技術,統一攔截sql,並真對大結果的查詢先做一次count查詢。

步驟一

1.1 定義攔截器PreCheckBigQueryInnerInterceptor

public class PreCheckBigQueryInnerInterceptor implements InnerInterceptor {}
1.2 重寫willDoQuery方法
 public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 解析sql
        Statement stmt = CCJSqlParserUtil.parse(boundSql.getSql());
        if (stmt instanceof Select) {
            PlainSelect selectStmt = (PlainSelect) ((Select) stmt).getSelectBody();
            if (Objects.nonNull(selectStmt.getLimit())) {
                //包含limit查詢
                return true;
            }
            for (SelectItem selectItem : selectStmt.getSelectItems()) {
                //計數查詢 count();
                SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
                if (selectExpressionItem.getExpression() instanceof Function) {
                    //包含function查詢
                    return true;
                }
            }
            Long aLong = doQueryCount(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            if (aLong == 0L) {
                return false;
            }
            if (aLong > 20) {
                throw new RuntimeException("單個查詢結果大於20條!!!");
            }
        }
        return true;
    }
1.3 程式碼解析
1.3.1 利用CCJSqlParserUtil解析sql,並判斷sql型別,只對Select的SQL攔擊.
1.3.2 對於已有limit的sql查詢,直接放行.
1.3.3 對於包含function查詢(例如count(1)計算,max()...),直接放行.
1.3.4 否則判斷為大結果查詢,執行(doQueryCount)與查詢數量.
1.3.5 對於大於指定數量的結果,丟擲異常.
1.4 定義doQueryCount方法
private Long doQueryCount(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        MappedStatement countMs = buildAutoCountMappedStatement(ms);
        String countSqlStr = autoCountSql(true, boundSql.getSql());
        PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
        BoundSql countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
        PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
        CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
        Object result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql).get(0);
        System.out.println(result);
        return (result == null ? 0L : Long.parseLong(result.toString()));
    }
程式碼解讀:參考PaginationInnerInterceptor(mybatis-plus)分頁外掛
1.4.1:構造MappedStatement物件buildAutoCountMappedStatement(ms),MappedStatement相當於一個儲存 SQL 語句、輸入引數和輸出結果對映等資訊的封裝體,它對應一條 SQL 語句,幷包含了該 SQL 語句執行所需的所有資訊。如下程式碼
<mapper namespace="com.example.UserMapper">
   <select id="selectAllUsers" resultType="com.example.User">
       SELECT * FROM user
   </select>
</mapper>

注意:必須重新構造,不能直接使用入參中的ms

1.4.2:autoCountSql(true, boundSql.getSql()) 定義並優化計數查詢語句
String.format("SELECT COUNT(1) FROM (%s) TOTAL", originalSql);
1.4.3: 執行查詢executor.query

步驟二

1.1 註冊攔截器PreCheckBigQueryInnerInterceptor

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//分頁外掛(Mybatis-plus)
    interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());//防止全表更新(Mybatis-plus)
    interceptor.addInnerInterceptor(new PreCheckBigQueryInnerInterceptor());//防止全表查詢(自定義外掛)
    return interceptor;
}

知識小結:

  1. MybatisPlusInterceptor
public class MybatisPlusInterceptor implements Interceptor {
    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();
}

​ 他是基於mybatis的Interceptor介面做的攔截器,上文中我們 註冊攔截器PreCheckBigQueryInnerInterceptor的攔截器其實新增到MybatisPlusInterceptor.interceptors集合中。

  1. 為啥重寫willDoQuery見程式碼而不是beforeQuery
 public Object intercept(Invocation invocation) throws Throwable {
       ......
                for (InnerInterceptor query : interceptors) {
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
     ......
        return invocation.proceed();
 }

2.1 willDoQuery先於beforeQuery方法,且一定會執行