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】那里打上断点,开始调试,传入一个中文字符串:

mybatis if test 等于忽略大小写_idea


可以看到我们传入的参数会被塞入第一个值,而作为比较的内容会被塞入第二个值。

mybatis if test 等于忽略大小写_intellij-idea_02


根据上图中 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 中,得到如下结果:

mybatis if test 等于忽略大小写_maven_03


这里的 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;
}

mybatis if test 等于忽略大小写_maven_04


mybatis if test 等于忽略大小写_idea_05


可以看到 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()">。

搞定收工。