关于MyBatis中if判断 0 和 ''相等的问题

问题:使用判断<if test='x != null and x != "" '>的时候x为Integer值 0 的时候不会进去判断。

原因:MyBatis的源码中对空传的判断是会先转成 0.0D 所以 0 = 0.0D是true的 判断就会进不去,可以更改条件:

<if test='x != null and x != "" or x == 0'>

源码流程分析:

例如下面代码:

  <select id="getLables" resultType="java.lang.String">
    SELECT distinct article_label from ARTICLE
    <if test="x != null and x != ''">
    where 1 = 1
  </if>
  </select>

当x传入值为 Integer 类型的 0 的时候,这个判断是进不去的。
那为什么进不去呢? 0不是null 也不是 '' 为什么这个判断进不去呢?

首先看下在服务启动的时候MyBatis是怎么处理我们的sql的:

MyBatis会把我们的sql语句进行预处理,并且存储在 mappedStatements 这个对象里
key就是mapper文件命名空间+'.'+id。
value 存储的是一个MappedStatement对象。

protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
image.png

上图下面的红框中,上面是在构造一个当前sql的MappedStatement对象,构造完成后调用configuration类的方法将对象添加到mappedStatements 这个map中去。

  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }

MappedStatement 这个类有一个SqlSource 变量


image.png

sql相关的信息都存储在这里面。

因为sql相关的信息都是在启动服务时候预编译好的,所以我们如果修改了xml中的sql相关内容,只通过编译是不会重新加载的,只有重启服务才可以。

看下代码运行时MyBatis的源码中的处理:

我的sql:

  <select id="getLables" resultType="java.lang.String">
    SELECT distinct article_label from ARTICLE
      <if test="x != null and x != ''">
        where 1 = 1
      </if>
  </select>

1、当运行查询的时候,首先会进入到MyBatis中的DynamicSqlSource类中,并把参数传递过来:


image.png

2、创建DynamicContext对象


image.png

3、使用rootSqlNode对象去解析参数并动态拼接出来sql
4、遍历不同的节点

例如:
StaticTextSqlNode 静态文本sql节点(SELECT distinct article_label from ARTICLE )
IfSqlNodeif 条件节点 等
通过遍历节点去往context 实例中设置值。

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }
}

5、进入IfSqlNode类的apply方法进行判断
调用表达式比较类的evaluateBoolean()方法判断两个表达式是否相等

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

6、在ExpressionEvaluator类中使用OgnlCache类的方法进行判断

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

7、在OgnlCache类中调用Ognl类的getValue方法,将表达式和参数全都传进去进行比较。
中间略过.........
8、调用ASTAnd类的getValueBody方法

    protected Object getValueBody(OgnlContext context, Object source) throws OgnlException {
        Object result = null;
        int last = this._children.length - 1;

        for(int i = 0; i <= last; ++i) {
            result = this._children[i].getValue(context, source);
            if (i != last && !OgnlOps.booleanValue(result)) {
                break;
            }
        }

        return result;
    }

这个children的值就是 if条件中test 的内容 拆分后的结果,如下:


image.png

9、调用OgnlOps类的equal方法

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

在这个方法内部调用了一个isEqual(v1, v2)方法:这个方法比较的isEqual(0, '')返回值是true,所以判定问题就是出在这里的。看下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) {
                    int i = 0;

                    for(int icount = Array.getLength(object1); result && i < icount; ++i) {
                        result = isEqual(Array.get(object1, i), Array.get(object2, i));
                    }
                }
            }
        } else {
            result = object1 != null && object2 != null && (object1.equals(object2) || compareWithConversion(object1, object2) == 0);
        }

        return result;
    }

因为 object1 显然不是空也不是数组,所以会进入最下面的else方法:
也就是

result = object1 != null && object2 != null && (object1.equals(object2) || compareWithConversion(object1, object2) == 0);

object1 != null 结果:true
object2 != null 结果:true
object1.equals(object2) 结果:false

那么问题就只有出现在这里面了:compareWithConversion 结果: true?
执行下面代码的时候 v1:0 v2:''
getNumericType(v1)和getNumericType(t1, t2, true)源码放下面了。

    public static int compareWithConversion(Object v1, Object v2) {
        int result;
        if (v1 == v2) {
            result = 0;
        } else {
            int t1 = getNumericType(v1); // 4
            int t2 = getNumericType(v2);//  10
            int type = getNumericType(t1, t2, true); // 10
            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;
    }

根据下面的值可以看到switch分支会走到 case 8:
会使用doubleValue(v2); 将v2 转换成double值

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

调用String s = stringValue(value, true);方法获取当前参数的String类型值,并且去掉空格。
如果s.length() == 0 直接返回了0.0D 所以最终结果:
v1 = 0.0D
v2 = 0.0D
v1 == v2 返回了true

结果:最后判断 <if test="x != null and x != ''">中的 0 != '' 是false 所以不会拼接进去我们的条件。

注意:getNumericType(v1) 这个方法是判断 v1的类型

public static int getNumericType(Object value) {
        if (value != null) {
            Class c = value.getClass();
            if (c == Integer.class) {
                return 4;
            }

            if (c == Double.class) {
                return 8;
            }

            if (c == Boolean.class) {
                return 0;
            }

            if (c == Byte.class) {
                return 1;
            }

            if (c == Character.class) {
                return 2;
            }

            if (c == Short.class) {
                return 3;
            }

            if (c == Long.class) {
                return 5;
            }

            if (c == Float.class) {
                return 7;
            }

            if (c == BigInteger.class) {
                return 6;
            }

            if (c == BigDecimal.class) {
                return 9;
            }
        }

        return 10;
    }
public static int getNumericType(int t1, int t2, boolean canBeNonNumeric) {
        if (t1 == t2) {
            return t1;
        } else if (canBeNonNumeric && (t1 == 10 || t2 == 10 || t1 == 2 || t2 == 2)) {
            return 10;
        } else {
            if (t1 == 10) {
                t1 = 8;
            }

            if (t2 == 10) {
                t2 = 8;
            }

            if (t1 >= 7) {
                if (t2 >= 7) {
                    return Math.max(t1, t2);
                } else if (t2 < 4) {
                    return t1;
                } else {
                    return t2 == 6 ? 9 : Math.max(8, t1);
                }
            } else if (t2 >= 7) {
                if (t1 < 4) {
                    return t2;
                } else {
                    return t1 == 6 ? 9 : Math.max(8, t2);
                }
            } else {
                return Math.max(t1, t2);
            }
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容