最近公司做了几次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; } 复制代码
我们进入 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表达式对空字符串的解析了。