一次mybatis中ognl引发的bug排查
现象
项目组一妹子程序员求助,说mybatis有bug,有一个值明明设置的是A.prop1=XXX,但是存到数据库里面却会自动变成A.prop1=true,尝试了各种调整也找不原因,都快急疯了!我以前确实没有研究过mybatis源码,本着专(ba)研(mei)问(zhuang)题(b)的精神,决定通过debug对mybais一探究竟。
定位
debugDao的入口类便是org.apache.ibatis.binding.MapperProxy ,可以看到它实现了InvocationHandler,很明显mybatis使用了jdk的动态代理,参见查看我以前关于动态代理类的培训ppt如下,那么它一定有地方使用了newProxyInstance生成代理类,果然在MapperProxyFactory中就可以找到对应的方法,只不过这次动态代理是对纯接口进行代理,而不是对实现类代理(当然满足动态代理中被代理类需要实现接口的要求了!)
进入到MapperMethod中的execute方法,可以看到mybatis对select、update、delete、insert有不同的分支处理:
进入到出问题的update方法中,可以定位到sqlSession.update执行时修改了传入的参数值,把XXX改成了true,这个update方法到底藏了什么玄机?继续进入,发现sqlSession也是spring sessionTemplate生成的一个动态代理,主要是增加获取链接和事物操作,通过代理层的操作,进入Mybatis的DefaultSqlSession中,接下来就是Mybatis预编译要动态生成sql语句了,在动态生成语句时终于最终定位到了罪魁祸首ifSqlNode.apply方法(整体调用栈见下图)!
解决
mybatis中会根据mapper文件生成一个SqlNodeTree,然后根据入参的数据有选择的生成最终的SQL,例如mapper文件中支持<if test>语法,if test的条件动态决定update或者where的sql语句是否存在,例如传过来的A对象中没有prop1属性,那么if test A.prop1!=null判断一下,如果没有的话,最终的sql就不包含update prop1=?,达到灵活和提高性能的目的。这个ifSqlNode就是mapper if test语法的具体Node,但是这个node应该只是判断是否存在这个条件啊!怎么会改值的?
进入到具体的判断方法内部,发现了原来这边判断if test是使用的Ognl表达式引擎啊,Ognl是一个功能非常强大的JAVA表达式引擎,但是由于过于强大了,导致使用它的Struts2漏洞漫天飞,你Http请求中传参'Runtime.getRuntime.exec("shutdown")',它真的就给你执行关机了你敢信!!。
/* */ public boolean evaluateBoolean(String expression, Object parameterObject) /* */ { /* 29 */ Object value = OgnlCache.getValue(expression, parameterObject); /* 30 */ if (value instanceof Boolean) return ((Boolean)value).booleanValue(); /* 31 */ if (value instanceof Number) return (!(new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO))); /* 32 */ return (value != null); /* */ }
敏锐的我对着expression看了一遍又一遍,突然,我眼花了么?
A.prop1=!null ! A.prop1=!null !! A.prop1=!null !!!
你妹啊! "!="写成了"=!"!ognl又一次立功了,在需要它判断的时候,它忠实的执行了赋值(怪OGNL不怪妹子是几个意思?),修改了mapper文件中的错误,终于恢复了正常!
扩展
上网搜索下看看有没有犯同样错误的同学,没想到还真有人在mybatis中这样玩OGNL的,根据if test判断的结果给table名赋值,无疑是提供了一种崭(hun)新(luan)的思路!!
给你们这些挖坑的跪了!
<if test="@tk.mybatis.mapper.util.OGNL@isNotDynamicParameter(_parameter) or dynamicTableName == null or dynamicTableName == ''"> select from ${dynamicTableName }