为什么Mybatis将Integer为0的属性解析成空串?

缘起

最近公司作了几回CodeReview,在你们一块儿看代码的过程当中,互相借鉴,学到了不少,也各自说了点平时遇到的所谓的“坑”,其中有一个同事遇到的问题,蛮有意思的。程序员

<if test="age != null and age != ''">  
     age = #{age} 
</if>
复制代码

在这个mapper文件中, age是Integer类型,若是age传的是0,通过表达式的断定,会由于不知足 age != '' 这个条件而跳过这条sql的拼接。sql

而下面这样写就是正确的:express

<if test="age != null">  
     age = #{age} 
</if>
复制代码

究竟是什么缘由致使的呢,网上说法不少,广泛的说法就是mybatis在解析的时候,会把 integer 的 0 值 和 '' 当作等价处理。apache

那究竟是基于什么样的缘由致使了mybatis这样的解析结果呢?博主回去之后就阅读了下源码一探究竟。缓存

原因

从GitHub上clone了一份最新的mybatis源码后,准备了以下的测试用例。bash

String resource = "org/apache/ibatis/zread/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //从 XML 中构建 SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession session = sqlSessionFactory.openSession();
    try {
        MybatisTableMapper mapper = session.getMapper(MybatisTableMapper.class);
        List<MybatisTable> mybatisTable = mapper.listByAge(0);
        System.out.println(mybatisTable);
    } finally {
        session.close();
    }
复制代码

准备工做Ok了,单步Debug走起来。微信

SqlSessionFactoryBuilder.build(InputStream inputStream, String environment, Properties properties)session

build 函数跳进去能够看到有 XMLConfigBuildermybatis

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
复制代码

一步步跳进去跟代码,根据执行流程能够看到,一开始mybatis先作了一些mapper的namespace,url等的解析,构建出一个Configuration类,再以此为基础,build构建出一个DefaultSqlSessionFactory,最后openSession 获取sqlSession, 固然这只是简单的梳理下大体的流程,mybatis真实的状况远比这个复杂,毕竟还要处理事务、回滚事务等transaction操做呢。app

好,如今mybatis的准备工做算是作完了,接下来就是重头戏了,mybatis是如何解析执行个人sql的呢?我们继续往下Debug

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
复制代码

默认的是SimpleExecutor,由于默认是开启缓存的,因此最终的执行器是CachingExecutor

executor = (Executor) interceptorChain.pluginAll(executor);
复制代码

这是mybatis中动态代理的运用,暂时不作深刻解析,咱们只要知道它会返回一个代理对象,在执行executor方法前,会执行拦截器。

最后一路debug,终于,咱们找到了DynamicSqlSource这个类,我顿时眼前一亮,继续debug下去,终于最后目标锁定了IfSqlNode类。

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

}
复制代码

能够看到,若是,evaluator.evaluateBoolean(test, context.getBindings()) 为true则拼接sql,不然就忽略。

继续跟进去发现mybatis居然用了OgnlCache进行获取值的,那么罪魁祸首或许就是这个OGNL表达式了(好古老的一个词汇了啊,博主小声念叨

Object value = OgnlCache.getValue(expression, parameterObject);
复制代码

博主顿时绝望了,由于已经看的十分疲惫了= =,没办法,继续debug下去

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;
    }
复制代码

在尝试了好几遍之后,博主终于定位到了关键代码(别问好几遍是多少遍!😭

ASTNotEq这个NotEq的比叫类中,他使用了本身的equal方法

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;
    }
复制代码

咱们进入compareWithConversion一看究竟发现:

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);
    }
复制代码

总结

如此看来,只要String的长度等于0的话,最终都会被解析为0.0,因此不只是Integer类型,Float型,Double型都会遇到相似的问题,最本质的问题仍是,OGNL表达式对空字符串的解析了。


知乎专栏:程序员Mk

微信公众号:程序员Mk

相关文章
相关标签/搜索