MyBatis if 標籤的坑,居然被我踩到了。。。

架構文摘發表於2020-12-09

事件的原因是這樣的,需求是按條件查資料然後給前端展示就行了,寫的時候想著挺簡單的,不就是使用 MyBatis 動態 SQL 去查詢資料嗎?

現實還是很殘酷的,等我寫完上完 UAT 後,前端同學說根據state查的資料與理想的資料不一致,這個state當時設計時只有兩個值:01

/**
 * 資料狀態
 */
@Range(min = 0, max = 1, message = "狀態只能為0(未處理),1(已處理)")
private Integer state;

理想情況下通過前端傳遞過來的值,然後進行sql查詢就可以了:

<if test="req.state != null and req.state != ''">
            AND md.state = #{req.state}
</if>

上面的sql首先判斷state不為空且不為空字串時,然後新增比較state欄位。初步看下來if判斷沒什麼問題,但是我傳遞進去的req.stateInteger型的,仔細檢視req.state != null沒毛病,然後發現req.state != ''使用Integer與空字串做比較。

前端在查詢的時如果沒有傳遞req.statereq.state != null 這裡不會滿足,但是前端傳遞了一個0過來的時候req.state != ''居然返回的是false也就是說在MyBatis的if語法中0是等於空字串的

{
	"state": 0
}

這樣的比較沒有報錯,也是有點想不通了,沒辦法只能去看MyBatis原始碼找出這原因。

檢視 MyBatis 原始碼

MyBatis 其他原始碼的查詢過程就不詳細說了,這裡直接找到XMLScriptBuilder類,找到if語法的解析過程,然後一步步的探究0 == ''的原因。 XMLScriptBuilder會解析trimif等 MyBatis 支援的語法,它的解析原理是通過NodeHandler來分別解析不同的標籤:

  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

由於是不正解的語法是if標籤,檢視IfHandler就好了,其他現在略過就好。

 private class IfHandler implements NodeHandler {
    public IfHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  }

MyBatis會將if標籤抽象成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;
  }

}

終於有一點眉頭了, MyBatis 會將if標籤的test屬性使用ExpressionEvaluator測試一下是否為true或者為false

public class ExpressionEvaluator {

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

  public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
    Object value = OgnlCache.getValue(expression, parameterObject);
    if (value == null) {
      throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
    }
    if (value instanceof Iterable) {
      return (Iterable<?>) value;
    }
    if (value.getClass().isArray()) {
        // the array may be primitive, so Arrays.asList() may throw
        // a ClassCastException (issue 209).  Do the work manually
        // Curse primitives! :) (JGB)
        int size = Array.getLength(value);
        List<Object> answer = new ArrayList<Object>();
        for (int i = 0; i < size; i++) {
            Object o = Array.get(value, i);
            answer.add(o);
        }
        return answer;
    }
    if (value instanceof Map) {
      return ((Map) value).entrySet();
    }
    throw new BuilderException("Error evaluating expression '" + expression + "'.  Return value (" + value + ") was not iterable.");
  }

}

最後得到結論:Mybatis 使用的 Ognl表示式 來獲取 test 屬性的值

最終論證

已經知道 MyBatis 內部是使用的 Ognl表示式 ,是不是 Ognl表示式 的引起的呢? 實踐一下就知道了,先引入依賴:

<!-- https://mvnrepository.com/artifact/ognl/ognl -->
<dependency>
    <groupId>ognl</groupId>
    <artifactId>ognl</artifactId>
    <version>2.7.3</version>
</dependency>

寫程式測試:

    public static void main(String[] args) {

        Map<String, Object> objectMap = new HashMap<>();
        objectMap.put("state", 0);
        Object value = OgnlCache.getValue("state == ''", objectMap);
        System.out.println(value);
    }

上面程式輸出的真的是true。。。

總結

真是腦袋抽筋啊,Integer還判斷是否為空字串。。。

記錄此坑,希望對大家有所幫助。

推薦

歡迎關注公眾號:架構文摘,獲得獨家整理120G的免費學習資源助力你的架構師學習之路!

公眾號後臺回覆arch028獲取資料:

相關文章