简易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;
    }
}

 

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