簡易SQL判別器

        在之前工作的慢SQL排查中,發現有一種慢查是這樣的,select * from tableName where 1=1,這個產生的原因相信大家都清楚,我在處理該類問題的時候,就想如何更好的避免這種問題,通過翻閱了PageHelper源碼,以及MySQL的插件原理,寫了一個簡易的判別器,提供一個解決該問題的思路。

        基本方法就是在執行sql的時候將其攔截,通過jsqlparser解析where條件進行判斷,如果只是其中只包含1=1(或者我們可以通過配置來指定,如果包含特定的狀態查詢,也等同與全表查詢,如where del=0等),就直接拋錯,避免去查庫造成慢查。

配置如下:

mybatis-config.xml中配置插件類

<plugins>
    <plugin interceptor="com.example.demo.util.SqlPrevention">
      <property name="specialStatus" value="del|sale"/>
    </plugin>
</plugins>

裏面的specialStatus就是特定的配置,後期也可以考慮在開發環境增加索引鍵判定用以校驗新寫的SQL是否匹配索引。

然後創建這個插件類,並實現MyBatis的Interceptor。

這裏僅對查詢方法進行了攔截,也可對刪除修改等方法做攔截,以避免誤刪,誤改操作。

@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}
)})
@Slf4j
public class SqlPrevention implements Interceptor {

    //demo演示用,存儲配置的屬性的
    Map<String, Object> map = Maps.newHashMap();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        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) {
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        //以上代碼都是copy自pageHelper的,下面開始判斷where條件是否存在相關問題
        String sql = boundSql.getSql().replaceAll("[\\s\n ]+", " ");
        log.info("boundSql={}", sql);
        if (!checkSql(sql)) {
            throw new RuntimeException("SQL異常");
        }
        //如果校驗正常,則放行
        return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);

    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        //獲取配置屬性,這裏用map是爲了demo演示用,比如pageHelper中採用的是GuavaCache來存儲的。
        map.put("specialStatus", properties.getProperty("specialStatus"));
    }

    public boolean checkSql(String sql) {
        CCJSqlParserManager parserManager = new CCJSqlParserManager();
        Select select;
        try {
            select = (Select) parserManager.parse(new StringReader(sql));
        } catch (JSQLParserException e) {
            return false;
        }
        PlainSelect plain = (PlainSelect) select.getSelectBody();
        Expression whereExpression = plain.getWhere();
        if (whereExpression == null) {
            return false;
        }
        //通過jsqlparser解析到where條件後,關鍵字會轉成大寫,這裏要統一處理
        String str = StringUtils.trimAllWhitespace(whereExpression.toString().toLowerCase());

        //如果只包含1=1,那麼不允許執行該SQL
        if ("1=1".equals(str)) {
            return false;
        }
        //將where條件中的數字刪除
        str = Pattern.compile("[0-9]").matcher(str).replaceAll("");
        String specialStatus = String.valueOf(map.get("specialStatus"));
        List<String> specialStatusList = Lists.newArrayList(specialStatus.split("\\|"));
        specialStatusList.add("=");
        specialStatusList.add("and");
        //將specialStatus中關鍵字和=,and以及數字刪除
        for (String s : specialStatusList) {
            str = str.replace(s, "");
        }
        //如果還有剩餘內容,則可以執行,否則不執行
        if (StringUtils.isEmpty(str)) {
            return false;
        }
        return true;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章