爲什麼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

相關文章
相關標籤/搜索