MyBatis参数为0时跟空字符串''判断相等的分析以及解决办法

  • 起因
    之前在一个Mapper文件中写了这样一个if标签,结果当参数为0时不论如何if标签的判断都是false,打印sql也发现当参数为0时这个条件会不起作用,因此查看了一下MyBatis中相关的源码,if标签如下:
        <if test="backCarType != null and backCarType != ''">
			and m.back_car = #{backCarType,jdbcType=BIT}
		if>
  • MyBatis 创建BoundSql的过程
    上文提到了MappedStatement这个类,这个类中有一个getBoundSql()方法,现在来跟踪一下这个方法:
  - MappedStatement.getBoundSql(Object parameterObject) 
  public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      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;
  }

  - DynamicSqlSource.getBoundSql(Object parameterObject)
  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);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

重点关注这个rootSqlNode,因为它就是解析我们写在mapper文件中的各种标签,比如if、choose、foreach等,接下来看一下是怎么解析if标签的:

public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  private final String test;
  private final SqlNode contents;

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

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

}

  - ExpressionEvaluator.evaluateBoolean(String expression, Object parameterObject)
  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)).compareTo(BigDecimal.ZERO) != 0;
    }
    return value != null;
  }

上面代码中出现了一个很重要的东西,Ognl,现在来关注一下Ognl怎么处理and的:

    - ASTAnd.getValueBody( OgnlContext context, Object source )
    protected Object getValueBody( OgnlContext context, Object source )
            throws OgnlException
    {
        Object result = null;
        int last = _children.length - 1;
        for ( int i=0; i <= last; ++i )
        {
            result = _children[i].getValue( context, source );

            if ( i != last && ! OgnlOps.booleanValue(result) )
                break;
        }
        
        return result;
    }

解析如下:
如之前示例的if标签,它会解析成两个_children节点:
1.backCarType != null
2.backCarType != ‘’
如果判断结束或者有一个为false的话会结束循环,然后将结果返回.
接下来看一下Ognl是怎么判断这两个_children节点的:

    - ASTNotEq.getValueBody( OgnlContext context, Object source )
    protected Object getValueBody( OgnlContext context, Object source ) throws OgnlException
    {
        Object v1 = _children[0].getValue( context, source );
        Object v2 = _children[1].getValue( context, source );
        
        return OgnlOps.equal( v1, v2 ) ? Boolean.FALSE : Boolean.TRUE;
    }

解释一下,这里的v1就是参数,v2就是判断值,当我们参数是0的时候,如我们示例的if标签,第一个_children节点判断的时候,v1是0,v2是null,第二个_children节点判断的时候,v1是0,v2是",重点关注一下第二种我们认为异常的情况:

    - OgnlOps.equal(Object v1, Object v2)
    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并不为null,然后会调用isEqual(v1, v2)方法进行判断:

    public static boolean isEqual(Object object1, Object object2)
    {
        boolean result = false;

        if (object1 == object2) {
            result = true;
        } else {
            if ((object1 != null) && object1.getClass().isArray()) {
                if ((object2 != null) && 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 {
                // Check for converted equivalence first, then equals() equivalence
                result = (object1 != null) && (object2 != null)
                        && (object1.equals(object2) || (compareWithConversion(object1, object2) == 0));
            }
        }
        return result;
    }

很显然,v1不等于v2,并且v1不为null也不是数组,因此决定权在这个表达式(compareWithConversion(object1, object2) == 0,观察一下这个方法:

    以下这些方法到在OgnlOps类中:

    public static int compareWithConversion(Object v1, Object v2)
    {
        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 instanceof Comparable) && v1.getClass().isAssignableFrom(v2.getClass())) {
                        result = ((Comparable) v1).compareTo(v2);
                        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;
    }

    public static int getNumericType(Object value)
    {
        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;
    }

    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);
    }

    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);
    }

    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;
    }

分析如下:
1.首先v1不等于v2,然后获取到v1的类型是BYTE,v2的类型是NONNUMERIC
2.然后将会以v2为主,接下来会进入switch语句
3.case到NONNUMERIC,然后发现t1是BYTE并不是NONNUMERIC,然后case到DOUBLE
4.在doubleValue方法中,首先判断类型,"“空字符串会判断s.length() == 0这时候显然返回true,然后”“字符串对等的double值就是0.0,
5.v1(0)的double值也是0.0,因此在Ognl中”"跟0是相等的

解决方法:

我们可以参照以下if标签的写法:

    <if test="backCarType == 0 or (backCarType != null and backCarType != '')">
	  and m.back_car = #{backCarType,jdbcType=BIT}
    if>

你可能感兴趣的