0.1. BUG概述
MyBatis的这个BUG,出现在条件判断标签中,当试图一个定义为String的属性跟一个英文单词比较时,就会出现bug,具体见下图:
这里我们定义的brandWord是String类型,当我们试图将其与单词 ‘Y’ 比较的时候,就会有下面的异常:
0.2. BUG源码解析
初看这个异常的时候,会感觉到很奇怪,为什么最终抛出的 NumberFormatException,即试图将字符串 “Y”解析为Double。为什么Mybatis在解析 if 标签的时候,要把 “Y” 解析为 Double类型呢?
关于这个问题,网上也有一些资料,但是感觉还是没有说到问题的关键。这些资料大都只是说明了解决方案,如把 ‘Y’改为’1’等,但是这并没有解释为什么会产生 NumberFormatException,即 mybatis 为何要把 “Y” 解析为 Double类型。 带着这个问题,我们来看mybatis解析的源码。还是那句话,源码是最好的学习资料,也是解决问题的最好资料,源码面前,一切没有秘密!
关于分析bug源码,我们可以看异常抛出的栈,顺着异常栈,由外往里看:
首先我们看到 DefaultSqlSession.selectList方法:
1 | public List selectList(String statement, Object parameter, RowBounds rowBounds) { |
在 executor.query(ms, wrapCollection(parameter), rowBounds, handler); 执行到BaseExecutor.class执行器中的query方法
1 | public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, |
在query的方法中看到boundSql,是通过 ms.getBoundSql(parameter);获取的。
再点进去可以看到MappedStatement.class类中的getBoundSql方法
1 | public BoundSql getBoundSql(Object parameterObject) { |
看到其中有sqlSource.getBoundSql(parameterObject); sqlsource是一个接口
1 | /** |
类中getBoundSql是一个核心方法,mybatis 也是通过这个方法来为我们构建sql。BoundSql 对象其中保存了经过参数解析,以及判断解析完成sql语句。比如 都回在这一层完成,具体的完成方法往下看,那最常用sqlSource的实现类是DynamicSqlSource.class
1 | public class DynamicSqlSource implements SqlSource { |
核心方法是调用了rootSqlNode.apply(context); rootSqlNode是一个接口
1 | public interface SqlNode { |
可以看到类中 rootSqlNode.apply(context); 的方法执行就是一个递归的调用,通过不同的 实现类执行不同的标签,每一次appll是完成了我们<></>一次标签中的sql创建,计算出标签中的那一段sql,mybatis通过不停的递归调用,来为我们完成了整个sql的拼接。那我们主要来看IF的实现类IfSqlNode.class
1 | public class IfSqlNode implements SqlNode { |
可以看到IF的实现中,执行了 if (evaluator.evaluateBoolean(test, context.getBindings())) 如果返回是false的话直接返回,否则继续递归解析IF标签以下的标签,并且返回true。那继续来看 evaluator.evaluateBoolean 的方法
1 | public class ExpressionEvaluator { |
关键点在于 OgnlCache.getValue,我们接着看它的代码:
1 | public static Object getValue(String expression, Object root) { |
可以看到,在OgnlCache.getValue中调用了Ognl.getValue,那我们接着来看Ognl.getValue。不过这里需要注意的一点是:由前面的异常栈图:
我们知道,OgnlCache.getValue紧接着调用的是 org.apache.ibatis.ognl.Ognl 这个类的getValue方法,而这个类的源码,mybatis本身并没有提供,它的源码在ibatis的源码包中。
1 | public static Object getValue(Object tree, Map context, Object root, Class resultType) throws OgnlException { |
接着又调用了SimpleNode以及ASTEq中的方法(这些方法的调用过程,可以在异常栈里面看到),然后又调用那个了OgnlOps类的equal方法。OgnlOps类对我们的分析比较重要,下面我们结合源码来详细说明。
0.3. OgnlOps类
0.3.1. equal方法
1 | public static boolean equal(Object v1, Object v2) { |
说明:代码运行到这里时,我们知道,v1 是String类型,值为 “Y”,而 v2 是 ‘Y’,ognl会认为它是Character类型,而不是String类型。
我们来看详细的代码,显然这里 v1 不等于 null,而且 v1 不等于 v2。看到代码说明一,这里会去执行 isEqual(v1, v2) 方法,我们来看这个方法的源码。
0.3.2. isEqual方法
1 | public static boolean isEqual(Object object1, Object object2) { |
显然,object1 != object2, 而且 (object1 != null) && (object2 != null)。看到代码说明二,这里又会调用compareWithConversion方法,那我们接着来看源码。
0.3.3. compareWithConversion方法
1 | public static int compareWithConversion(Object v1, Object v2, boolean equals) { |
看到代码说明三,这里会对每个对象调用getNumericType方法,还是看源码;
0.3.4. getNumericType方法
1 | public static int getNumericType(Object value) { |
这个方法的作用,就是根据传进来的对象value,得到它对应的数值型的值,如果value不能转化为数值型,那么返回NONNUMERIC。
好了,我们回到代码说明三:
1 | //代码说明三 |
我们知道,这里的v1是String类型,那么 t1 的值是 NONNUMERIC; v2 是 Character类型,那么 t2 的值是 CHAR。 对应的type值,我们接着看源码:
1 | public static int getNumericType(int t1, int t2, boolean canBeNonNumeric) { |
很明显,type的值为NONNUMERIC。我们从代码说明三接着往下看:
1 | public static int compareWithConversion(Object v1, Object v2, boolean equals) { |
因为type的值是NONNUMERIC,所以执行到代码说明四,这时 t1 的值为 NONNUMERIC,但是 t2 的值为 CHAR,所以 if直接跳过,问题就出现在这。我们知道,在switch case语句中,执行了一个case后,如果没有 break 语句,会接着执行后面的case,也就是说 代码说明五 那里的 case FLOAT,case DOUBLE都会被执行。看到这里,是不是对前面的 “mybatis 为什么要把 “Y” 解析为 Double类型呢”这个问题有点眉目了呢?别着急,还请耐着性子,接着往下看代码!
1 | double dv1 = doubleValue(v1), |
这里会求 v1(String类型,值为”Y”)的double value,我们来看doubleValue方法的代码:
0.3.5. doubleValue方法
1 | public static double doubleValue(Object value) throws NumberFormatException { |
因为value是String类型,值为 “Y”,看到代码说明六的stringValue方法
0.3.6. stringValue方法
1 | public static String stringValue(Object value, boolean trim) { |
很明显,传进来的value是String,调用了 toString方法后,返回的值还是String,即这里返回的是String “Y”。我们接着看代码说明七:
1 | //代码说明七 |
代码看到这里,一切都一目了然了:Double.parseDouble(“Y”) 当然会抛异常。
好了,漫长的源码分析之旅完成了,下面总结下出现这个bug(或者异常)的原因: mybatis之所以会出现这个异常,我觉得主要还是在代码说明四那里:
0.3.7. 异常原因
当一个非数值型和一个数值型比较时,mybatis想着把他们都转化成Double来比较,而这个非数值型又不能被解析为Double,这时候,异常就产生了。
所以这也就能解释为什么 下面这种情况可以通过:
当 brandWord = “1”时,字符串 “1”可以被解析为Double,就没有异常了。 总的来说,对于上述的异常,有以下几种解决办法: