在前面兩篇的MyBatis源碼解讀中,我們一路跟蹤到了MapperProxy,知道了盡管是使用了動態代理技術使得我們能直接使用接口方法。為鞏固加深動態代理,我們不妨再來回憶一遍何為動態代理。
我相信在初學MyBatis的時候幾乎每個人都會發出一個疑問,為什么明明是XXXDao接口,我沒有用任何代碼實現這個接口,但卻能直接使用這個接口的方法。現在清楚了,動態代理。我們來寫一個demo小程序來看看。
首先是一個Test.java的接口,只有一個say方法。
1 package day_16_proxy; 2 3 /** 4 * @author 余林豐 5 * 6 * 2016年11月16日 7 */ 8 public interface Test { 9 void say(); 10 }
我們現在想像MyBatis那樣不用實現它而是直接調用。
test.say();
當然不可能直接實例化一個接口,此時就需要生成一個代理類TestProxy.java,這個類需實現InvocationHandler接口。
1 package day_16_proxy; 2 3 import java.lang.reflect.InvocationHandler; 4 import java.lang.reflect.Method; 5 6 /** 7 * @author 余林豐 8 * 9 * 2016年11月16日 10 */ 11 public class TestProxy implements InvocationHandler { 12 13 /* (non-Javadoc) 14 * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) 15 */ 16 @Override 17 public Object invoke(Object proxy, Method method, Object[] args) 18 throws Throwable { 19 if (method.getName().equals("say")){ 20 System.out.println("hello world"); 21 } 22 return null; 23 } 24 25 }
需要實現invoke方法,意思就是“調用”的意思(當然我們想要調用接口中的方法時並不會顯示調用invoke)。我們從第19行看到,當調用的方法是say時,輸出“hello world”。有了這個TestProxy.java代理類過后,我們再來客戶端代碼中測試。
1 package day_16_proxy; 2 3 import java.lang.reflect.Proxy; 4 5 /** 6 * @author 余林豐 7 * 8 * 2016年11月16日 9 */ 10 public class Client { 11 12 public static void main(String[] args){ 13 Test test = (Test)Proxy.newProxyInstance(Test.class.getClassLoader(), new Class<?>[]{Test.class}, new TestProxy()); 14 test.say(); 15 } 16 }
在第14行代碼中,我們已經能夠直接調用Test接口中的say方法了,原因就在於我們通過Proxy.newProxyInstance方法生成了一個代理類實例即TestProxy。
回到我們的MyBatis源碼,在上一節中我們知道了一個Dao接口實際上是通過MapperProxyFactory生成了一個MapperProxy代理類。
1 //org.apache.ibatis.binding.MapperProxyFactory 2 protected T newInstance(MapperProxy<T> mapperProxy) { 3 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); 4 }
第3行代碼是不是和在開頭的例子中客戶端測試代碼如出一轍?生成代理類,這個代理類就是MapperProxy。清楚了MyBatis是如何構造出代理類的算是解決了第一個問題——一個接口怎么能直接調用其方法。
現在拋出第二個問題——接口中每個具體的方法是如何做到一一實現代理的呢?我們再來看看MapperProxy類。這次我們先看MapperProxy類實現的InvocationHanlder.invoke方法。
1 //org.apache.ibatis.binding.MapperProxy 2 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 3 if (Object.class.equals(method.getDeclaringClass())) { 4 try { 5 return method.invoke(this, args); 6 } catch (Throwable t) { 7 throw ExceptionUtil.unwrapThrowable(t); 8 } 9 } 10 final MapperMethod mapperMethod = cachedMapperMethod(method); 11 return mapperMethod.execute(sqlSession, args); 12 }
第3行做一個判斷,判斷是否是一個類,如果是一個類,那么久直接傳遞方法和參數調用即可。但我們知道此時是一個接口(也可以自己實現接口,舊版本通常這樣做)。如果不是一個類的話,就會創建一個MapperMethod方法。見名思意:好像就是這個類在執行我們所調用的每一個接口方法。最后返回的是MapperMethod.execute方法。暫時不予理會MapperProxy類中的cachedMapperMethod方法。
來看看MapperMethod類,這個MapperMethod類就不得了了啊,可以說它是統管所有和數據庫打交道的方法(當然概括起來也只有insert、delete、update、select四個方法)。所以,不管你的dao層有多少方法,歸結起來的sql語句都有且僅有只有insert、delete、update、select,可以預料在MapperMethod的execute方法中首先判斷是何種sql語句。
//org.apache.ibatis.binding.MapperMethod public Object execute(SqlSession sqlSession, Object[] args) { switch (command.getType()) { case INSERT: {……} case UPDATE: {……} case DELETE: {……} case SELECT: {……} case FLUSH: {……} } }
這是MepperMethod.execute方法的刪減,我們可以看到確實在execute方法內部首先判斷是何種sql語句。(注意:在閱讀這部分源代碼時,我們的主線是MyBatis是如何創建出一個代理類,以及實現其方法的,而暫時忽略其中的細節)
我們選擇常見的"SELECT"sql語句來進行解讀,而在"SELECT"語句中又會設計到較多的細節問題:
1 //org.apache.ibatis.binding 2 case SELECT: 3 if (method.returnsVoid() && method.hasResultHandler()) { 4 executeWithResultHandler(sqlSession, args); 5 result = null; 6 } else if (method.returnsMany()) { 7 result = executeForMany(sqlSession, args); 8 } else if (method.returnsMap()) { 9 result = executeForMap(sqlSession, args); 10 } else if (method.returnsCursor()) { 11 result = executeForCursor(sqlSession, args); 12 } else { 13 Object param = method.convertArgsToSqlCommandParam(args); 14 result = sqlSession.selectOne(command.getName(), param); 15 } 16 break;
我們選取第7行中的executeForMany中的方法來解讀試試看。
1 //org.apache.ibatis.binding.MapperMethod 2 private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { 3 List<E> result; 4 Object param = method.convertArgsToSqlCommandParam(args); 5 if (method.hasRowBounds()) { 6 RowBounds rowBounds = method.extractRowBounds(args); 7 result = sqlSession.<E>selectList(command.getName(), param, rowBounds); 8 } else { 9 result = sqlSession.<E>selectList(command.getName(), param); 10 } 11 // issue #510 Collections & arrays support 12 if (!method.getReturnType().isAssignableFrom(result.getClass())) { 13 if (method.getReturnType().isArray()) { 14 return convertToArray(result); 15 } else { 16 return convertToDeclaredCollection(sqlSession.getConfiguration(), result); 17 } 18 } 19 return result; 20 }
第7行和第9行代碼就是我們真正執行sql語句的地方,原來兜兜轉轉它又回到了sqlSession的方法中。在下一節,我們再重新回到重要的SqlSession中。