美文网首页Java架构技术进阶
MyBatis if 标签的坑,居然被我踩到了。。。

MyBatis if 标签的坑,居然被我踩到了。。。

作者: Java入门到入坟 | 来源:发表于2020-12-10 15:05 被阅读0次

    推荐阅读:

    事件的原因是这样的,需求是按条件查数据然后给前端展示就行了,写的时候想着挺简单的,不就是使用 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还判断是否为空字符串。。。

    记录此坑,希望对大家有所帮助。

    相关文章

      网友评论

        本文标题:MyBatis if 标签的坑,居然被我踩到了。。。

        本文链接:https://www.haomeiwen.com/subject/dpuhgktx.html