在之前工作的慢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;
}
}