MyBatis-Bug源码解析

0.1. BUG概述

MyBatis的这个BUG,出现在条件判断标签中,当试图一个定义为String的属性跟一个英文单词比较时,就会出现bug,具体见下图:

mybatis-bug

这里我们定义的brandWord是String类型,当我们试图将其与单词 ‘Y’ 比较的时候,就会有下面的异常:

mybatis-bug

0.2. BUG源码解析

初看这个异常的时候,会感觉到很奇怪,为什么最终抛出的 NumberFormatException,即试图将字符串 “Y”解析为Double。为什么Mybatis在解析 if 标签的时候,要把 “Y” 解析为 Double类型呢?

关于这个问题,网上也有一些资料,但是感觉还是没有说到问题的关键。这些资料大都只是说明了解决方案,如把 ‘Y’改为’1’等,但是这并没有解释为什么会产生 NumberFormatException,即 mybatis 为何要把 “Y” 解析为 Double类型。 带着这个问题,我们来看mybatis解析的源码。还是那句话,源码是最好的学习资料,也是解决问题的最好资料,源码面前,一切没有秘密!

关于分析bug源码,我们可以看异常抛出的栈,顺着异常栈,由外往里看:

首先我们看到 DefaultSqlSession.selectList方法:

1
2
3
4
5
6
7
8
9
10
11
public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter),
rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

在 executor.query(ms, wrapCollection(parameter), rowBounds, handler); 执行到BaseExecutor.class执行器中的query方法

1
2
3
4
5
6
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

在query的方法中看到boundSql,是通过 ms.getBoundSql(parameter);获取的。

再点进去可以看到MappedStatement.class类中的getBoundSql方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public BoundSql getBoundSql(Object parameterObject) {  
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.size() <= 0) {
boundSql = new BoundSql(configuration, boundSql.getSql(),
parameterMap.getParameterMappings(), parameterObject);
}

// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}

return boundSql;
}

看到其中有sqlSource.getBoundSql(parameterObject); sqlsource是一个接口

1
2
3
4
5
6
7
8
9
10
/** 
*
* This bean represets the content of a mapped statement read from an XML file
* or an annotation. It creates the SQL that will be passed to the database out
* of the input parameter received from the user.
*
*/
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}

类中getBoundSql是一个核心方法,mybatis 也是通过这个方法来为我们构建sql。BoundSql 对象其中保存了经过参数解析,以及判断解析完成sql语句。比如 都回在这一层完成,具体的完成方法往下看,那最常用sqlSource的实现类是DynamicSqlSource.class

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
public class DynamicSqlSource implements SqlSource {  

private Configuration configuration;
private SqlNode rootSqlNode;

public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}

public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class
: parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType,
context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}

核心方法是调用了rootSqlNode.apply(context); rootSqlNode是一个接口

1
2
3
public interface SqlNode {  
boolean apply(DynamicContext context);
}

可以看到类中 rootSqlNode.apply(context); 的方法执行就是一个递归的调用,通过不同的 实现类执行不同的标签,每一次appll是完成了我们<></>一次标签中的sql创建,计算出标签中的那一段sql,mybatis通过不停的递归调用,来为我们完成了整个sql的拼接。那我们主要来看IF的实现类IfSqlNode.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IfSqlNode implements SqlNode {  
private ExpressionEvaluator evaluator;
private String test;
private SqlNode contents;

public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}

public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}

return false;
}
}

可以看到IF的实现中,执行了 if (evaluator.evaluateBoolean(test, context.getBindings())) 如果返回是false的话直接返回,否则继续递归解析IF标签以下的标签,并且返回true。那继续来看 evaluator.evaluateBoolean 的方法

1
2
3
4
5
6
7
8
9
public class ExpressionEvaluator {  
public boolean evaluateBoolean(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) return (Boolean) value;
if (value instanceof Number)
return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);

return value != null;
}

关键点在于 OgnlCache.getValue,我们接着看它的代码:

1
2
3
4
5
6
7
8
public static Object getValue(String expression, Object root) {
try {
Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}

可以看到,在OgnlCache.getValue中调用了Ognl.getValue,那我们接着来看Ognl.getValue。不过这里需要注意的一点是:由前面的异常栈图:
mybatis-bug
我们知道,OgnlCache.getValue紧接着调用的是 org.apache.ibatis.ognl.Ognl 这个类的getValue方法,而这个类的源码,mybatis本身并没有提供,它的源码在ibatis的源码包中。

1
2
3
4
5
6
7
8
9
10
11
public static Object getValue(Object tree, Map context, Object root, Class resultType) throws OgnlException {
Object result;
OgnlContext ognlContext = (OgnlContext) addDefaultContext(root, context);

result = ((Node) tree).getValue(ognlContext, root);
if (resultType != null) {
result = getTypeConverter(context).convertValue(context, root, null, null, result, resultType);
}

return result;
}

接着又调用了SimpleNode以及ASTEq中的方法(这些方法的调用过程,可以在异常栈里面看到),然后又调用那个了OgnlOps类的equal方法。OgnlOps类对我们的分析比较重要,下面我们结合源码来详细说明。

0.3. OgnlOps类

0.3.1. equal方法

1
2
3
4
5
6
7
8
9
public static boolean equal(Object v1, Object v2) {
if (v1 == null)
return v2 == null;
if (v1 == v2 || isEqual(v1, v2))//代码说明一
return true;
if (v1 instanceof Number && v2 instanceof Number)
return ((Number) v1).doubleValue() == ((Number) v2).doubleValue();
return false;
}

说明:代码运行到这里时,我们知道,v1 是String类型,值为 “Y”,而 v2 是 ‘Y’,ognl会认为它是Character类型,而不是String类型。

我们来看详细的代码,显然这里 v1 不等于 null,而且 v1 不等于 v2。看到代码说明一,这里会去执行 isEqual(v1, v2) 方法,我们来看这个方法的源码。

0.3.2. isEqual方法

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
public static boolean isEqual(Object object1, Object object2) {
boolean result = false;

if (object1 == object2) {
result = true;
} else {
if ((object1 != null) && (object2 != null)) {
if (object1.getClass().isArray() && object2.getClass().isArray() && (object2.getClass() == object1.getClass())) {
result = (Array.getLength(object1) == Array.getLength(object2));
if (result) {
for (int i = 0, icount = Array.getLength(object1); result && (i < icount); i++) {
result = isEqual(Array.get(object1, i), Array.get(object2, i));
}
}
} else {
if ((object1 != null) && (object2 != null)) {
// Check for converted equivalence first, then equals() equivalence
//代码说明二
result = (compareWithConversion(object1, object2, true) == 0) || object1.equals(object2);
}
}
}
}

return result;
}

显然,object1 != object2, 而且 (object1 != null) && (object2 != null)。看到代码说明二,这里又会调用compareWithConversion方法,那我们接着来看源码。

0.3.3. compareWithConversion方法

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
public static int compareWithConversion(Object v1, Object v2, boolean equals) {
int result;

if (v1 == v2) {
result = 0;
} else {

//代码说明三
int t1 = getNumericType(v1),
t2 = getNumericType(v2),
type = getNumericType(t1, t2, true);

switch (type) {
case BIGINT:
result = bigIntValue(v1).compareTo(bigIntValue(v2));
break;

case BIGDEC:
result = bigDecValue(v1).compareTo(bigDecValue(v2));
break;

case NONNUMERIC:
if ((t1 == NONNUMERIC) && (t2 == NONNUMERIC)) {
if ((v1 == null) || (v2 == null)) {
result = (v1 == v2) ? 0 : 1;
} else {
if (v1.getClass().isAssignableFrom(v2.getClass()) || v2.getClass().isAssignableFrom(v1.getClass())) {
if (v1 instanceof Comparable) {
result = ((Comparable) v1).compareTo(v2);
break;
} else {
if (equals) {
result = v1.equals(v2) ? 0 : 1;
break;
}
}
}
if (equals) {
// Equals comparison between non-numerics that are not of a common
// superclass return not equal
result = 1;
break;
} else {
throw new IllegalArgumentException("invalid comparison: " + v1.getClass().getName() + " and " + v2.getClass().getName());
}
}
}
// else fall through
case FLOAT:
case DOUBLE:
double dv1 = doubleValue(v1),
dv2 = doubleValue(v2);

return (dv1 == dv2) ? 0 : ((dv1 < dv2) ? -1 : 1);

default:
long lv1 = longValue(v1),
lv2 = longValue(v2);

return (lv1 == lv2) ? 0 : ((lv1 < lv2) ? -1 : 1);
}
}
return result;
}

看到代码说明三,这里会对每个对象调用getNumericType方法,还是看源码;

0.3.4. getNumericType方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static int getNumericType(Object value) {
int result = NONNUMERIC;

if (value != null) {
Class c = value.getClass();
if (c == Integer.class) return INT;
if (c == Double.class) return DOUBLE;
if (c == Boolean.class) return BOOL;
if (c == Byte.class) return BYTE;
if (c == Character.class) return CHAR;
if (c == Short.class) return SHORT;
if (c == Long.class) return LONG;
if (c == Float.class) return FLOAT;
if (c == BigInteger.class) return BIGINT;
if (c == BigDecimal.class) return BIGDEC;
}

return NONNUMERIC;
}

这个方法的作用,就是根据传进来的对象value,得到它对应的数值型的值,如果value不能转化为数值型,那么返回NONNUMERIC。

好了,我们回到代码说明三:

1
2
3
4
//代码说明三
int t1 = getNumericType(v1),
t2 = getNumericType(v2),
type = getNumericType(t1, t2, true);

我们知道,这里的v1是String类型,那么 t1 的值是 NONNUMERIC; v2 是 Character类型,那么 t2 的值是 CHAR。 对应的type值,我们接着看源码:

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
public static int getNumericType(int t1, int t2, boolean canBeNonNumeric) {
if (t1 == t2)
return t1;

//返回的是这一段代码的值
if (canBeNonNumeric && (t1 == NONNUMERIC || t2 == NONNUMERIC || t1 == CHAR || t2 == CHAR))
return NONNUMERIC;

if (t1 == NONNUMERIC) t1 = DOUBLE; // Try to interpret strings as doubles…
if (t2 == NONNUMERIC) t2 = DOUBLE; // Try to interpret strings as doubles…

if (t1 >= MIN_REAL_TYPE) {
if (t2 >= MIN_REAL_TYPE)
return Math.max(t1, t2);
if (t2 < INT)
return t1;
if (t2 == BIGINT)
return BIGDEC;
return Math.max(DOUBLE, t1);
} else if (t2 >= MIN_REAL_TYPE) {
if (t1 < INT)
return t2;
if (t1 == BIGINT)
return BIGDEC;
return Math.max(DOUBLE, t2);
} else
return Math.max(t1, t2);
}

很明显,type的值为NONNUMERIC。我们从代码说明三接着往下看:

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
public static int compareWithConversion(Object v1, Object v2, boolean equals) {
int result;

if (v1 == v2) {
result = 0;
} else {

//代码说明三
int t1 = getNumericType(v1),
t2 = getNumericType(v2),
type = getNumericType(t1, t2, true);

switch (type) {
case BIGINT:
result = bigIntValue(v1).compareTo(bigIntValue(v2));
break;

case BIGDEC:
result = bigDecValue(v1).compareTo(bigDecValue(v2));
break;

//代码说明四
case NONNUMERIC:
//下面这个if直接跳过了
if ((t1 == NONNUMERIC) && (t2 == NONNUMERIC)) {
if ((v1 == null) || (v2 == null)) {
result = (v1 == v2) ? 0 : 1;
} else {
if (v1.getClass().isAssignableFrom(v2.getClass()) || v2.getClass().isAssignableFrom(v1.getClass())) {
if (v1 instanceof Comparable) {
result = ((Comparable) v1).compareTo(v2);
break;
} else {
if (equals) {
result = v1.equals(v2) ? 0 : 1;
break;
}
}
}
if (equals) {
// Equals comparison between non-numerics that are not of a common
// superclass return not equal
result = 1;
break;
} else {
throw new IllegalArgumentException("invalid comparison: " + v1.getClass().getName() + " and " + v2.getClass().getName());
}
}
}
// else fall through
//代码说明五
case FLOAT:
case DOUBLE:
double dv1 = doubleValue(v1),
dv2 = doubleValue(v2);

return (dv1 == dv2) ? 0 : ((dv1 < dv2) ? -1 : 1);

default:
long lv1 = longValue(v1),
lv2 = longValue(v2);

return (lv1 == lv2) ? 0 : ((lv1 < lv2) ? -1 : 1);
}
}
return result;
}

因为type的值是NONNUMERIC,所以执行到代码说明四,这时 t1 的值为 NONNUMERIC,但是 t2 的值为 CHAR,所以 if直接跳过,问题就出现在这。我们知道,在switch case语句中,执行了一个case后,如果没有 break 语句,会接着执行后面的case,也就是说 代码说明五 那里的 case FLOAT,case DOUBLE都会被执行。看到这里,是不是对前面的 “mybatis 为什么要把 “Y” 解析为 Double类型呢”这个问题有点眉目了呢?别着急,还请耐着性子,接着往下看代码!

1
2
double dv1 = doubleValue(v1),
dv2 = doubleValue(v2);

这里会求 v1(String类型,值为”Y”)的double value,我们来看doubleValue方法的代码:

0.3.5. doubleValue方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static double doubleValue(Object value) throws NumberFormatException {
if (value == null)
return 0.0;
Class c = value.getClass();

if (c.getSuperclass() == Number.class)
return ((Number) value).doubleValue();

if (c == Boolean.class)
return ((Boolean) value).booleanValue() ? 1 : 0;

if (c == Character.class)
return ((Character) value).charValue();

//代码说明六
String s = stringValue(value, true);

//代码说明七
return (s.length() == 0) ? 0.0 : Double.parseDouble(s);

}

因为value是String类型,值为 “Y”,看到代码说明六的stringValue方法

0.3.6. stringValue方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public static String stringValue(Object value, boolean trim) {
String result;

if (value == null) {
result = OgnlRuntime.NULL_STRING;
} else {
result = value.toString();
if (trim) {
result = result.trim();
}
}
return result;
}

很明显,传进来的value是String,调用了 toString方法后,返回的值还是String,即这里返回的是String “Y”。我们接着看代码说明七:

1
2
//代码说明七
return (s.length() == 0) ? 0.0 : Double.parseDouble(s);

代码看到这里,一切都一目了然了:Double.parseDouble(“Y”) 当然会抛异常。

好了,漫长的源码分析之旅完成了,下面总结下出现这个bug(或者异常)的原因: mybatis之所以会出现这个异常,我觉得主要还是在代码说明四那里:

0.3.7. 异常原因

当一个非数值型和一个数值型比较时,mybatis想着把他们都转化成Double来比较,而这个非数值型又不能被解析为Double,这时候,异常就产生了

所以这也就能解释为什么 下面这种情况可以通过:
mybatis-bug

当 brandWord = “1”时,字符串 “1”可以被解析为Double,就没有异常了。 总的来说,对于上述的异常,有以下几种解决办法:

0.3.8. 解决办法

mybatis-bug