Mybatis之<if test="str == '0'">执行时报NumberFormatException的原因跟踪
- 一、报错内容
- 二、原因跟踪
- 三、总结
一、报错内容
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.NumberFormatException: For input string: "中文"
### Cause: java.lang.NumberFormatException: For input string: "中文"
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:92)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440)
at com.sun.proxy.$Proxy107.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:223)
... // 省略
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.NumberFormatException: For input string: "中文"
### Cause: java.lang.NumberFormatException: For input string: "中文"
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
... // 省略
Caused by: java.lang.NumberFormatException: For input string: "中文"
... // 省略
at org.apache.ibatis.ognl.OgnlOps.compareWithConversion(OgnlOps.java:100)
at org.apache.ibatis.ognl.OgnlOps.isEqual(OgnlOps.java:149)
at org.apache.ibatis.ognl.OgnlOps.equal(OgnlOps.java:808)
at org.apache.ibatis.ognl.ASTEq.getValueBody(ASTEq.java:52)
at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212)
at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:258)
at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:560)
at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:524)
at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:46)
at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:32)
at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:34)
... // 省略
二、原因跟踪
1、从上面报错内容中的第 7 行点击进入【SqlSessionTemplate.java:223】,看到如下方法:
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.sqlSessionProxy.selectList(statement, parameter);
}
2、根据方法【this.sqlSessionProxy.selectList】一路跳转到报错内容中的第 14 行【DefaultSqlSession.java:140】,看到如下方法:
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
3、接着根据方法【this.selectList】一路跳转到报错内容中的第 28 行【IfSqlNode.java:34】,看到如下方法:
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
4、再根据方法【evaluator.evaluateBoolean】一路跳转到报错内容中的第 22 行【SimpleNode.java:212】,看到如下方法:
protected Object evaluateGetValueBody(OgnlContext context, Object source) throws OgnlException {
context.setCurrentObject(source);
context.setCurrentNode(this);
if (!this._constantValueCalculated) {
this._constantValueCalculated = true;
boolean constant = this.isConstant(context);
if (constant) {
this._constantValue = this.getValueBody(context, source);
}
this._hasConstantValue = constant;
}
return this._hasConstantValue ? this._constantValue : this.getValueBody(context, source);
}
5、最后根据方法【this.getValueBody】一路跳转到报错内容中的第 21 行【ASTEq.java:52】,看到如下方法:
protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {
Object v1 = this._children[0].getValue(context, source);
Object v2 = this._children[1].getValue(context, source);
return OgnlOps.equal(v1, v2) ? Boolean.TRUE : Boolean.FALSE;
}
6、我们在【return】那里打上断点,开始调试,传入一个中文字符串:
可以看到我们传入的参数会被塞入第一个值,而作为比较的内容会被塞入第二个值。
根据上图中 v1 和 v2 的类型我们基本上能够确认报错原因了,这两个值的类型是不一样的,在后续的比较中结果肯定是 false 的。
7、通过断点进入【OgnlOps.equal(v1, v2)】这个比较方法:
public static boolean equal(Object v1, Object v2) {
if (v1 == null) {
return v2 == null;
} else if (v1 != v2 && !isEqual(v1, v2)) {
if (v1 instanceof Number && v2 instanceof Number) {
return ((Number)v1).doubleValue() == ((Number)v2).doubleValue();
} else {
return false;
}
} else {
return true;
}
}
可以看到,首先 v1 不等于 null,就会进入 else if 的判断,我们继续断点进入【isEqual(v1, v2)】的比较方法:
public static boolean isEqual(Object object1, Object object2) {
boolean result = false;
if (object1 == object2) {
result = true;
} else if (object1 != null && object2 != null) {
int i;
int icount;
if (object1.getClass().isArray()) {
if (object2.getClass().isArray() && object2.getClass() == object1.getClass()) {
result = Array.getLength(object1) == Array.getLength(object2);
if (result) {
i = 0;
for(icount = Array.getLength(object1); result && i < icount; ++i) {
result = isEqual(Array.get(object1, i), Array.get(object2, i));
}
}
}
} else {
i = getNumericType(object1);
icount = getNumericType(object2);
if (i != 10 || icount != 10 || object1 instanceof Comparable && object2 instanceof Comparable) {
result = compareWithConversion(object1, object2) == 0;
} else {
result = object1.equals(object2);
}
}
}
return result;
}
断点一路走到最后一个 else 中,得到如下结果:
这里的 t1 就是 i,t2 就是 icount,可以看到两个值不相等,相比较肯定是 false。
8、断点进入【compareWithConversion(object1, object2)】方法:
public static int compareWithConversion(Object v1, Object v2) {
int result;
if (v1 == v2) {
result = 0;
} else {
int t1 = getNumericType(v1);
int t2 = getNumericType(v2);
int type = getNumericType(t1, t2, true);
switch(type) {
case 6:
result = bigIntValue(v1).compareTo(bigIntValue(v2));
break;
case 9:
result = bigDecValue(v1).compareTo(bigDecValue(v2));
break;
case 10:
if (t1 == 10 && t2 == 10) {
if (v1 instanceof Comparable && v1.getClass().isAssignableFrom(v2.getClass())) {
result = ((Comparable)v1).compareTo(v2);
break;
}
throw new IllegalArgumentException("invalid comparison: " + v1.getClass().getName() + " and " + v2.getClass().getName());
}
case 7:
case 8:
double dv1 = doubleValue(v1);
double dv2 = doubleValue(v2);
return dv1 == dv2 ? 0 : (dv1 < dv2 ? -1 : 1);
default:
long lv1 = longValue(v1);
long lv2 = longValue(v2);
return lv1 == lv2 ? 0 : (lv1 < lv2 ? -1 : 1);
}
}
return result;
}
可以看到 type 的值为 10,会进入 case 10 的 if 判断,到这里我们已经完全确认报错原因了,由于 t2 等于 2,if 判断为 false ,而 case 10 的 if 体外没有 break ,导致程序会继续往下执行,case 7 为空,直接跳过,进入 case 8,各位仁兄已经猜到了,我们进入【doubleValue(v1)】看看:
public static double doubleValue(Object value) throws NumberFormatException {
if (value == null) {
return 0.0D;
} else {
Class c = value.getClass();
if (c.getSuperclass() == Number.class) {
return ((Number)value).doubleValue();
} else if (c == Boolean.class) {
return (Boolean)value ? 1.0D : 0.0D;
} else if (c == Character.class) {
return (double)(Character)value;
} else {
String s = stringValue(value, true);
return s.length() == 0 ? 0.0D : Double.parseDouble(s);
}
}
}
好家伙,【Double.parseDouble(s)】,它在给我们把字符串强转成 Double,不报错才怪,一路点进去,找到了报错输出内容:
if (var6 == var18) {
throw new NumberFormatException("For input string: \"" + var0 + "\"");
}
到此结案。
三、总结
我们回过头来看这个标签:
<if test="str == ‘0’">。
如果传入的参数不是中文或者英文,而是数字(我们就传 0),那么执行到 case 8 时是不会报错的,但是 t1 和 t2 的值依旧是不相等的,我们永远得不到 true 的结果。
为什么 0 ≠ 0 ?
那是因为 if 标签中的 test 语句内容我们用的是双引号,而 0 是用单引号包裹的,在 Ognl 表达式中,双引号字符串会被解析为 char 数组,而单引号单字符内容会被解析为 Character,由于类型不相等,结果永远是 false。
注意:当单引号包裹的内容超过 1 个字符时,比如 ‘01’,解析出来的反而是 char 数组,这时进行相同内容比较时结果会为 true。
但是我们不建议这么做,我们的解决方案是,将引号互换,即双引号换成单引号,单引号换成双引号,那么修改后的标签是这样子的:
<if test='str == “0”'>。
或者,如果你有强迫症,前面的代码中已经是双引号包含单引号的写法了,你又不想单独修改这条而导致前后不一致,那么就改成这样:
<if test="str == ‘0’.toString()">。
搞定收工。