xiaobaoqiu Blog

Think More, Code Less

MyBatis空where拦截器

最近项目中出现了至少两次因为Mybatis的动态where条件不满足导致实际sql语句的where条件为空,进而查询全表,当数据量比较大的时候,导致OOM的情况.

如何禁止这种情况,个人觉得三种措施:

  • 1.在逻辑层面加充分的参数有效性检查;
  • 2.在where条件中如果索引条件都不满足,加上1=2这种必然失败的条件;
1
2
3
4
5
6
7
8
9
10
11
<where>
    <choose>
        <when test="id != null">
            ...
        </when>
        ...
        <otherwise>
        and 1=2
        </otherwise>
    </choose>
</where>
  • 3.Mybatis拦截器;

前两种措施都是依赖人,从这个层面讲,是不靠谱的,即一个策略不是强制的,就是不靠谱的.相对而言,第三种是不依赖程序员的自觉性,是最靠谱的.乘周六有时间,实现一个简单的Mybatis拦截器来拦截where条件为空的SQL语句.

1.实现

先上代码,这里拦截了SqlCommandType为select,update,delete这三种类型.出现情况目前只是打日志.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
 * Mybatis拦截器, 拦截:
 *      select语句where条件为空
 *      update语句where条件为空
 *      delete语句where条件为空
 *
 * 处理: 目前只是打warning日志,因为项目中存在数据量很少的表读全表的情况(比如加载数据进缓存)
 *      TODO:后续考虑设置拦截器的白名单,不在白名单的触发where条件为空的直接抛出异常.
 *
 * @author: xiaobaoqiu  Date: 15-8-22 Time: 上午10:26
 */
@Intercepts({ @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = { Connection.class }) })
public class EmptyWhereInterceptor implements Interceptor {

    private static final Logger logger = LoggerFactory.getLogger(EmptyWhereInterceptor.class);

    /**
     * 拦截的 COMMAND 类型
     */
    private static final Set<String> INTERCEPTOR_COMMAND = Sets.newHashSet("select", "update", "delete");

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();

        //获取实际的StatementHandler
        if (handler instanceof RoutingStatementHandler) {
            handler = (BaseStatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
        }

        //获取SqlCommandType
        String commandType = getCommandType(handler);

        if (INTERCEPTOR_COMMAND.contains(commandType)) {
            String originSql = handler.getBoundSql().getSql().toLowerCase();  //获取sql
            if (!originSql.contains("where")) {
                logger.warn("禁止使用不带where条件的SQL语句.原始SQL={}", originSql);
            }
        }

        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * 获取Command类型,小写化返回
     *
     * @param handler
     * @return
     */
    private String getCommandType(StatementHandler handler) {
        MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(handler, "mappedStatement");
        return mappedStatement.getSqlCommandType().toString().toLowerCase();
    }
}

包括一个简单反射工具的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
 * 反射工具
 *
 * @author: xiaobaoqiu  Date: 15-8-22 Time: 上午11:52
 */
public class ReflectUtil {
    public ReflectUtil() {
    }

    /**
     * 改变 Accessible,便于访问private等属性
     * @param field
     */
    private static void makeAccessible(Field field) {
        if(!Modifier.isPublic(field.getModifiers())) {
            field.setAccessible(true);
        }
    }

    /**
     * 获取 object 的字段,字段名称为filedName,获取不到返回null
     * @param object
     * @param filedName
     * @return
     */
    private static Field getDeclaredField(Object object, String filedName) {
        Class superClass = object.getClass();

        while(superClass != Object.class) {
            try {
                return superClass.getDeclaredField(filedName);
            } catch (NoSuchFieldException var4) {
                superClass = superClass.getSuperclass();
            }
        }

        return null;
    }

    /**
     * 获取object字段fieldName的值,如果字段不存在直接抛异常
     *
     * @param object
     * @param fieldName
     * @return
     */
    public static Object getFieldValue(Object object, String fieldName) {
        Field field = getDeclaredField(object, fieldName);
        if(field == null) {
            throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
        } else {
            makeAccessible(field);
            Object result = null;

            try {
                result = field.get(object);
            } catch (IllegalAccessException var5) {
                var5.printStackTrace();
            }

            return result;
        }
    }

    /**
     * 设置object字段fieldName的值,如果字段不存在直接抛异常
     *
     * @param object
     * @param fieldName
     * @param value
     */
    public static void setFieldValue(Object object, String fieldName, Object value) {
        Field field = getDeclaredField(object, fieldName);
        if(field == null) {
            throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
        } else {
            makeAccessible(field);

            try {
                field.set(object, value);
            } catch (IllegalAccessException var5) {
                var5.printStackTrace();
            }

        }
    }
}

2.Mybatis的Interceptor原理

这篇文章很好的解释了Mybatis的Interceptor机制.

mybatis读取配置再xml文件中Interceptor,通过反射构造其实例,将所有的Interceptor保存到InterceptorChain中。

mybatis的拦截器只能代理指定的四个类:ParameterHandler、ResultSetHandler、StatementHandler以及Executor。

参考:

http://www.tuicool.com/articles/RbyUfu

http://blog.csdn.net/hupanfeng/article/details/9247379