前言
IllegalSQLInnerInterceptor:不规范SQL拦截器。
由于开发人员水平参差不齐,即使订了开发规范很多人也不遵守,SQL是影响系统性能最重要的因素,所以拦截掉不规范SQL语句。
拦截SQL类型的场景:
1、 必须使用到索引,包含leftjoin连接字段,符合索引最左原则;
- 如果因为动态SQL,bug导致update的where条件没有带上,全表更新上万条数据
- 如果检查到使用了索引,SQL性能基本不会太差
1、 SQL尽量单表执行,有查询leftjoin的语句,必须在注释里面允许该SQL运行,否则会被拦截,有leftjoin的语句,如果不能拆成单表执行的SQL,请leader商量在做,SQL尽量单表执行的好处:;
- 查询条件简单、易于开理解和维护
- 扩展性极强;(可为分库分表做准备)
- 缓存利用率高
1、 where条件为空;
2、 where条件使用了!=;
3、 where条件使用了not关键字;
4、 where条件使用了or关键字;
5、 where条件使用了使用子查询;
测试案例
1、 配置类添加插件;
// 添加sql性能规范插件
interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
return interceptor;
1、 Mapper添加SQL;
<select id="list" resultType="org.seata.service.order.entity.OrderTbl">
select * from order_tbl
</select>
1、 测试执行,发现此SQL没有添加条件,被拦截;
2、 可使用注解@InterceptorIgnore(illegalSql=“1”)忽略此插件;
@InterceptorIgnore(illegalSql = "1")
List<OrderTbl> list();
源码分析
1、 beforePrepare:前置处理;
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpStatementHandler = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpStatementHandler.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
// INSERT语句、添加了拦截忽略注解、存在SQL 解析缓存时不执行校验
if (sct == SqlCommandType.INSERT || InterceptorIgnoreHelper.willIgnoreIllegalSql(ms.getId())
|| SqlParserHelper.getSqlParserInfo(ms)) return;
BoundSql boundSql = mpStatementHandler.boundSql();
String originalSql = boundSql.getSql();
logger.debug("检查SQL是否合规,SQL:" + originalSql);
String md5Base64 = EncryptUtils.md5Base64(originalSql);
// 查询缓存是否已校验过
if (cacheValidResult.contains(md5Base64)) {
logger.debug("该SQL已验证,无需再次验证,,SQL:" + originalSql);
return;
}
parserSingle(originalSql, connection);
// 缓存验证结果
cacheValidResult.add(md5Base64);
}
1、 processSelect:where条件校验,根据SQL类型,进入不同的校验方法;
protected void processSelect(Select select, int index, String sql, Object obj) {
// 使用JsqlParser解析SQL为select对象
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
Expression where = plainSelect.getWhere();
// 断言是否有where条件
Assert.notNull(where, "非法SQL,必须要有where条件");
Table table = (Table) plainSelect.getFromItem();
List<Join> joins = plainSelect.getJoins();
// 校验where
validWhere(where, table, (Connection) obj);
// 校验Join
validJoins(joins, table, (Connection) obj);
}
1、 validWhere:验证where条件的字段,是否有not、or等等,并且where的第一个字段,必须使用索引;
private void validWhere(Expression expression, Table table, Table joinTable, Connection connection) {
validExpression(expression);
if (expression instanceof BinaryExpression) {
//获得左边表达式
Expression leftExpression = ((BinaryExpression) expression).getLeftExpression();
validExpression(leftExpression);
//如果左边表达式为Column对象,则直接获得列名
if (leftExpression instanceof Column) {
Expression rightExpression = ((BinaryExpression) expression).getRightExpression();
if (joinTable != null && rightExpression instanceof Column) {
if (Objects.equals(((Column) rightExpression).getTable().getName(), table.getAlias().getName())) {
validUseIndex(table, ((Column) rightExpression).getColumnName(), connection);
validUseIndex(joinTable, ((Column) leftExpression).getColumnName(), connection);
} else {
validUseIndex(joinTable, ((Column) rightExpression).getColumnName(), connection);
validUseIndex(table, ((Column) leftExpression).getColumnName(), connection);
}
} else {
//获得列名
validUseIndex(table, ((Column) leftExpression).getColumnName(), connection);
}
}
//如果BinaryExpression,进行迭代
else if (leftExpression instanceof BinaryExpression) {
validWhere(leftExpression, table, joinTable, connection);
}
//获得右边表达式,并分解
Expression rightExpression = ((BinaryExpression) expression).getRightExpression();
validExpression(rightExpression);
}
}
1、 validUseIndex:检查是否使用索引;
private void validUseIndex(Table table, String columnName, Connection connection) {
//是否使用索引
boolean useIndexFlag = false;
String tableInfo = table.getName();
//表存在的索引
String dbName = null;
String tableName;
String[] tableArray = tableInfo.split("\\.");
if (tableArray.length == 1) {
tableName = tableArray[0];
} else {
dbName = tableArray[0];
tableName = tableArray[1];
}
List<IndexInfo> indexInfos = getIndexInfos(dbName, tableName, connection);
for (IndexInfo indexInfo : indexInfos) {
if (null != columnName && columnName.equalsIgnoreCase(indexInfo.getColumnName())) {
useIndexFlag = true;
break;
}
}
if (!useIndexFlag) {
throw new MybatisPlusException("非法SQL,SQL未使用到索引, table:" + table + ", columnName:" + columnName);
}
}
1、 validJoins:如果SQL用了leftJoin,验证是否有or、not等等,并且验证是否使用了索引;
private void validJoins(List<Join> joins, Table table, Connection connection) {
//允许执行join,验证jion是否使用索引等等
if (joins != null) {
for (Join join : joins) {
Table rightTable = (Table) join.getRightItem();
Expression expression = join.getOnExpression();
validWhere(expression, table, rightTable, connection);
}
}
}
1、 验证通过后,会进入后续执行流程,否则会抛出异常;
总结
使用此插件,可以一定程度上规范SQL书写,也可以拓展自己的SQL规范逻辑。可以在开发环境中配置此插件,便于规范