最近公司作了幾回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
函數跳進去能夠看到有 XMLConfigBuilder
類mybatis
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