转载

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

最近公司做了几次CodeReview,在大家一起看代码的过程中,互相借鉴,学到了很多,也各自说了点平时遇到的所谓的“坑”,其中有一个同事遇到的问题,蛮有意思的。

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

在这个mapper文件中, age是Integer类型,如果age传的是0,经过表达式的判定,会因为不满足 age != '' 这个条件而跳过这条sql的拼接。

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

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

到底是什么原因导致的呢,网上说法很多,普遍的说法就是mybatis在解析的时候,会把 integer 的 0 值 和 '' 当做等价处理。

那到底是基于什么样的原因导致了mybatis这样的解析结果呢?博主回去以后就阅读了下源码一探究竟。

缘由

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

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)

build 函数跳进去可以看到有 XMLConfigBuilder

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

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

好,现在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;
    }
复制代码

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

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;
    }
复制代码
为何Mybatis将Integer为0的属性解析成空串?

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

为何Mybatis将Integer为0的属性解析成空串?
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表达式对空字符串的解析了。

原文  https://juejin.im/post/5da46d316fb9a04e054d942c
正文到此结束
Loading...