MyBatis實戰之解析與執行 MyBatis之反射技術+JDK動態代理+cglib代理 MyBatis之反射技術+JDK動態代理+cglib代理 MyBatis之反射技術+JDK動態代理+cglib代理
本次所談的原理僅僅只涉及基本的框架和核心程式碼,並不會全部都說到,比如關於MyBatis是如何解析XML檔案和其他配置檔案從而的到內容,還有就是JDBC如何使用,關於JDBC如何使用,可以參考我的這篇部落格:單例模式和JDBC
還有就是關於Java基礎方面的內容,個人建議大家抽空看看《Java程式設計思想》這本書,這本書可以作為一本參考書來看,不要從頭開始看,有選擇的閱讀是最好的,從頭開始看,別說看懂問題,估計你看的都想睡覺了。另外最好的話還是可以通過知識付費看看人家是怎麼講Java的,通過知識付費獲取的Java相關知識,應該是不錯的,比如在極客時間這個APP中看到的楊曉峰《Java核心技術36講》,口碑目前還不錯,不過建議朋友們最好還是自己時不時根據一些參考書或者官網再加上自己工作用到時刻複習一下,總結一下。這樣還是有很大好處的。
MyBatis的執行分為兩大部分,第一部分是讀取配置檔案快取到Configuration物件,用以建立SqlSessionFactory,第二部分是SqlSession的執行過程。相對而言,SqlSessionFactory的建立比較容易理解,而SqlSession的執行過程遠遠不是那麼簡單了,它將包括許多複雜的技術,我們需要討論反射技術和動態代理技術,這是揭示MyBatis底層架構的基礎。
當我們掌握了MyBatis的執行原理,我們就可以知道MyBatis是怎麼執行的,同時當我們在深入理解MyBatis相關的原始碼和涉及到的設計模式後,我們也許就能像MyBatis-Plus的開發者那樣,開發出一個比MyBatis或者MyBatis-Plus還要好的持久層框架。
一、涉及的技術難點簡介
Mapper僅僅只是一個介面,而不是一個包含邏輯的實現類。我們知道一個介面是沒有辦法去執行的,那麼它是怎麼執行的呢?這不是違反教科書上說的介面不能執行的道理嗎?相信不少初學者會對此有疑惑。
答案就是動態代理。
首先,什麼是代理模式?所謂的代理模式就是在原有的服務商多加以佔位,通過這個佔位去控制服務的訪問。這句話不太容易理解,舉例而言,假設你是一個公司的工程師,能提供一些技術服務,公司的客服就一個美女,她不懂技術。而我是一個客戶需要你們公司提供技術服務。顯然,我只會找到你們的客服,和客服溝通,而不是找你溝通。客服會根據公司的規章制度和業務規則來決定找不找你服務。那麼這個時候客服就等同於你的一個代理,她通過和我的交流來控制對你的訪問,當然她也可以提供一些你們公司對外的服務。而我只能通過她的代理訪問你。對我而言,根本不需要認識你,只需要認識客服就可以了。事實上,站在我的角度,我會認為客服就代表你們公司,而不管真正為我服務的你是怎麼樣的。
其次,為什麼要使用代理模式?通過代理,一方面可以控制如何訪問真正的服務物件,提供額外服務。另外一方面有機會通過重寫一些類來滿足特定的需要,正如客服也可以根據公司的業務規則,提供一些服務,這個時候就不需要勞你大駕。
動態代理示意圖:
一般而言,動態代理分為兩種,一種是JDK反射機制提供的代理,另一種是CGLIB代理。在JDK提供的代理,我們必須要提供介面,而CGLIB則不需要提供介面,在MyBatis裡面兩種動態代理技術都已經使用了。但是在此之前我們需要學習的技術就是反射。
1.反射技術
關於反射技術詳細可以參考我的這篇部落格:MyBatis之反射技術+JDK動態代理+cglib代理
不過在此基礎上,我還是要說說什麼是反射?
JAVA反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意方法和屬性;這種動態獲取資訊以及動態呼叫物件方法的功能稱為java語言的反射機制。 JAVA反射(放射)機制:“程式執行時,允許改變程式結構或變數型別,這種語言稱為 動態語言”。從這個觀點看,Perl,Python,Ruby是動態語言,C++,Java,C#不是動態語言。但是JAVA有著一個非常突出的動態相關機制:Reflection,用在Java身上指的是我們可以於執行時載入、探知、使用編譯期間完全未知的classes。換句話說,Java程式可以載入一個執行時才得知名稱的class,獲悉其完整構造(但不包括methods定義),並生成其物件實體、或對其fields設值、或喚起其methods。 2.JDK動態代理 JDK的動態代理,是由JDK的java.lang,reflect.*包提供支援的,我們需要完成這麼幾個步驟? (1)編寫服務類和介面,這個是真正的服務提供者,在JDK代理中介面是必須的; (2)編寫代理類,提供繫結和代理的方法; JDK的代理最大的缺點是需要提供介面,而MyBatis的Mapper就是一個介面,它採用的就是JDK的動態代理。 關於示例還是可以參考我的這篇部落格(雖然前面提到過): MyBatis之反射技術+JDK動態代理+cglib代理 3.CGLIB動態代理 JDK提供的動態代理存在一個缺陷,就是你必須提供接口才可以使用,為了克服這個缺陷,我們可以使用開源框架-CGLIB,它是一種流行的動態代理。 關於示例還是可以參考我的這篇部落格: MyBatis之反射技術+JDK動態代理+cglib代理 二、構建SqlSessionFactory過程 SqlSessionFactory是MyBatis的核心類之一,其最重要的功能就是提供建立MyBatis的核心介面SqlSession,所以我們需要先建立SqlSessionFactory,為此我們需要提供配置檔案和相關的引數。而MyBatis是一個複雜的系統,採用構造模式去建立SqlSessionFactory,我們可以通過SqlSessionFactoryBeanBuilder去構建。構建分為兩步: 第一步,通過org.apache.ibatis.builder.xml.XMLConfigBuilder解析配置的XML檔案,讀出配置引數,並將讀取的資料存入這個org.apache.ibatis.session.Configuration類中。注意,MyBatis幾乎所有的配置都是存在這裡的。 第二步,使用Configuration物件去建立SqlSessionFactory。MyBatis中的SqlSessionFactory是一個介面,而不是實現類,為此MyBatis提供了一個預設的SqlSessionFactory實現類,我們一般都會使用它org.apache.ibatis.session.defaults.DefaultSqlSessionFactory。注意,在大部分情況下我們都沒有必要自己去建立新的SqlSessionFactory的實現類。 這種建立的方式就是一種Builder模式。對於複雜的物件而已,直接使用構造方法構建是有困難的,這會導致大量的邏輯放在構造方法中,由於物件的複雜性,在構建的時候,我們更希望一步一步有秩序的來構建它,從而降低其複雜性。這個時候使用一個引數類總領全域性。例如,Configuration類,然後分佈構建,例如,DefaultSqlSessionFactory類,就可以構建一個複雜的物件,例如SqlSessionFactory,這種方式值得我們在工作中學習和使用。 1.構建Configuration 在SqlSessionFactory構建中,Configuration是最重要的,它的作用如下: (1)讀取配置檔案,包括基礎配置的XML和對映器的XML檔案; (2)初始化基礎配置,比如MyBatis的別名等,一些重要的類物件,例如外掛、對映器、ObjectFactory和typeHandler物件; (3)提供單例,為後續建立SessionFactory服務並提供配置的引數; (4)執行一些重要的物件方法,初始化配置資訊; 顯然Configuration不會是一個很簡單的類,MyBatis的配置資訊都會來自於此。有興趣的朋友可以讀讀原始碼,幾乎所有的配置都可以在這裡找到蹤影。 比如在 MyBatis實戰之初步 提到的單例 Configuration是通過XMLConfigBuilder去構建。首先,MyBatis會讀出所有的XML配置的資訊。然後,將這些資訊儲存到Configuration類的單例中。它會做如下初始化。 a.properties全域性引數; b.settings設定; c.typeAliases別名: d.typeHandler型別處理器; e.ObjectFactory物件; f.plugin外掛 g.environment環境; h.DatabaseIdProvier資料庫標識; i.Mapper對映器; 2.對映器的內部組成 一般而言,一個對映器是由3個部分組成: (1)MappedStatement,它儲存對映器的一個節點。包括許多我們配置的SQL、SQL的id、快取資訊、resultMap、parameterType、resultType、languageDriver等重要配置內容; (2)SqlSource,它是提供BoundSql物件的地方,它是MappedStatement的一個屬性; (3)BoundSql,它是建立SQL和引數的地方。它有3個常用的屬性:SQL、parameterObject、parameterMappings; 這些都是對映器的重要內容,也是MyBatis的核心內容。在外掛的應用中常常會用到它們。對映器的解析過程是比較複雜的,但是在大部分的情況下,我們並不需要去理會解析和組裝SQL的規則,因為大部分的外掛只要做很小的改變即可,無需做很大的改變。大的改變可能導致重寫這些內容。所以我們主要關注引數和SQL。 對映器的組成部分,如圖所示:注意,這張圖並沒有將所有的方法和屬性都列舉出來,只列舉了主要的屬性和方法。
MappedStatement物件涉及的東西較多,我們一般都不去修改它,因為容易產生不必要的錯誤。SqlSource是一個介面,它的主要作用是根據引數和其他的規則組裝SQL。這些都是很複雜的東西,好在MyBatis本身已經實現了它,一般也不需要去修改它。對於引數和SQL而言,主要的規則都反映在BoundSql類物件上,在外掛中往往需要拿到它進而可以拿到當前執行的SQL的引數以及引數規則,做出適當的修改,來滿足我們特殊的需求。
BoundSql會提供3個主要的屬性:parameterMappings、paramterObject和sql。
(1)其中parameterObject為引數本身,前面我們說到過,引數可以是簡單物件,Pojo、Map或者@Param註解的引數,由於它在外掛中相當常用,後面我們有必要討論一下它的規則;
(2)傳遞簡物件(包括int、String、float、double等),比如當我們傳遞int型別時,MyBatis會把引數變為Integer物件傳遞,類似的long、String、float、double也是如此;
(3)如果我們傳遞的是Pojo或者Map,那麼這個parameterObject就是你傳入的Pojo或者Map不變;
(4)當然我們也可以傳遞多個引數,如果沒有@Param註解,那麼MyBatis就會把parameterObject變為一個Map<String,Object>物件,其鍵值的關係是按順序來規劃的;
(5)如果我們使用@Param註解,那麼MyBatis就會把parameterObject變為一個Map<String,Object>物件,類似於沒有@Param註解,只是把其數字的鍵值對應置換為@Param註解的鍵值;
(6)parameterMappings,它是一個List,每個元素都是ParameterMapping的物件。這個物件會描述我們的引數。引數包括屬性、名稱、表示式、javaType、jdbcType、typeHandler等重要資訊,我們一般不需要去改變它。通過它可以實現引數和SQL的結合,以便PreparedStatement能夠通過它找到parameterObject物件的屬性並設定引數,使得程式準確執行;
(7)sql屬性就是我們書寫在對映器裡面的一條SQL,在大多數時候無需修改它,只有在外掛的情況下,我們可以根據需要進行改寫。改寫SQL將是一件危險的事情,請務必慎重行事;
3.構建SqlSessionFactory
有了Configuration物件構建SqlSessionFactory就很簡單了,我們只要寫很簡短的程式碼便可以了。
例如:
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
MyBatis會根據Configuration的配置讀取所配置的資訊,構建SqlSessionFactory物件。
三、SqlSession執行過程
SqlSession的執行過程是整個MyBatis最難以理解的部分。SqlSession是一個介面,使用它並不複雜。我們構建SqlSessionFactory就可以輕易地拿到SqlSession了。SqlSession給出了查詢、插入、更新、刪除的方法,在舊版本的MyBatis或iBatis中常常使用這些介面方法,而在新版的MyBatis中我們建議使用Mapper,所以它就是MyBatis最為常用和重要的介面之一。
SqlSession內部並沒有那麼容易,因為它的內部實現相當複雜。
1.對映器的動態代理
Mapper對映是通過動態代理來實現的,我們來看看程式碼清單:
MapperProxyFactory原始碼如下:
/** * Copyright 2009-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.binding; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.ibatis.session.SqlSession; /** * @author Lasse Voss */ public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
這裡我們可以看到動態代理對介面的繫結,它的作用就是生成動態代理物件(佔位)。
而代理的方法則被放到MapperProxy類中。
MapperProxy原始碼如下:
/** * Copyright 2009-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.binding; import java.io.Serializable; import java.lang.invoke.MethodHandles; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Map; import org.apache.ibatis.lang.UsesJava7; import org.apache.ibatis.reflection.ExceptionUtil; import org.apache.ibatis.session.SqlSession; /** * @author Clinton Begin * @author Eduardo Macarron */ public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; } @UsesJava7 private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class .getDeclaredConstructor(Class.class, int.class); if (!constructor.isAccessible()) { constructor.setAccessible(true); } final Class<?> declaringClass = method.getDeclaringClass(); return constructor .newInstance(declaringClass, MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC) .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args); } /** * Backport of java.lang.reflect.Method#isDefault() */ private boolean isDefaultMethod(Method method) { return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC && method.getDeclaringClass().isInterface(); } }
上面運用了invoke方法。一旦mapper是一個代理物件,那麼它就會執行到invoke方法裡面,invoke首先判斷它是否是一個類,顯然這裡Mapper是一個介面而不是類,所以判定失敗。那麼就會生成MapperMethod物件,它是通過cachedMapperMethod方法對其初始化的,然後執行execute方法,把sqlSession和當前執行的引數傳遞進去。
這個exexute方法的原始碼如下:
/** * Copyright 2009-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ibatis.binding; import org.apache.ibatis.annotations.Flush; import org.apache.ibatis.annotations.MapKey; import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.mapping.StatementType; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.ParamNameResolver; import org.apache.ibatis.reflection.TypeParameterResolver; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.SqlSession; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.*; /** * @author Clinton Begin * @author Eduardo Macarron * @author Lasse Voss */ public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; } private Object rowCountResult(int rowCount) { final Object result; if (method.returnsVoid()) { result = null; } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) { result = rowCount; } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) { result = (long)rowCount; } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) { result = rowCount > 0; } else { throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType()); } return result; } private void executeWithResultHandler(SqlSession sqlSession, Object[] args) { MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName()); if (!StatementType.CALLABLE.equals(ms.getStatementType()) && void.class.equals(ms.getResultMaps().get(0).getType())) { throw new BindingException("method " + command.getName() + " needs either a @ResultMap annotation, a @ResultType annotation," + " or a resultType attribute in XML so a ResultHandler can be used as a parameter."); } Object param = method.convertArgsToSqlCommandParam(args); if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args)); } else { sqlSession.select(command.getName(), param, method.extractResultHandler(args)); } } private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { List<E> result; Object param = method.convertArgsToSqlCommandParam(args); if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.<E>selectList(command.getName(), param, rowBounds); } else { result = sqlSession.<E>selectList(command.getName(), param); } // issue #510 Collections & arrays support if (!method.getReturnType().isAssignableFrom(result.getClass())) { if (method.getReturnType().isArray()) { return convertToArray(result); } else { return convertToDeclaredCollection(sqlSession.getConfiguration(), result); } } return result; } private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) { Cursor<T> result; Object param = method.convertArgsToSqlCommandParam(args); if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.<T>selectCursor(command.getName(), param, rowBounds); } else { result = sqlSession.<T>selectCursor(command.getName(), param); } return result; } private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) { Object collection = config.getObjectFactory().create(method.getReturnType()); MetaObject metaObject = config.newMetaObject(collection); metaObject.addAll(list); return collection; } @SuppressWarnings("unchecked") private <E> Object convertToArray(List<E> list) { Class<?> arrayComponentType = method.getReturnType().getComponentType(); Object array = Array.newInstance(arrayComponentType, list.size()); if (arrayComponentType.isPrimitive()) { for (int i = 0; i < list.size(); i++) { Array.set(array, i, list.get(i)); } return array; } else { return list.toArray((E[])array); } } private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) { Map<K, V> result; Object param = method.convertArgsToSqlCommandParam(args); if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey(), rowBounds); } else { result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey()); } return result; } public static class ParamMap<V> extends HashMap<String, V> { private static final long serialVersionUID = -2212268410512043556L; @Override public V get(Object key) { if (!super.containsKey(key)) { throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet()); } return super.get(key); } } public static class SqlCommand { private final String name; private final SqlCommandType type; public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { final String methodName = method.getName(); final Class<?> declaringClass = method.getDeclaringClass(); MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); if (ms == null) { if (method.getAnnotation(Flush.class) != null) { name = null; type = SqlCommandType.FLUSH; } else { throw new BindingException("Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName); } } else { name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } } } public String getName() { return name; } public SqlCommandType getType() { return type; } private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) { String statementId = mapperInterface.getName() + "." + methodName; if (configuration.hasStatement(statementId)) { return configuration.getMappedStatement(statementId); } else if (mapperInterface.equals(declaringClass)) { return null; } for (Class<?> superInterface : mapperInterface.getInterfaces()) { if (declaringClass.isAssignableFrom(superInterface)) { MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration); if (ms != null) { return ms; } } } return null; } } public static class MethodSignature { private final boolean returnsMany; private final boolean returnsMap; private final boolean returnsVoid; private final boolean returnsCursor; private final Class<?> returnType; private final String mapKey; private final Integer resultHandlerIndex; private final Integer rowBoundsIndex; private final ParamNameResolver paramNameResolver; public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); if (resolvedReturnType instanceof Class<?>) { this.returnType = (Class<?>) resolvedReturnType; } else if (resolvedReturnType instanceof ParameterizedType) { this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType(); } else { this.returnType = method.getReturnType(); } this.returnsVoid = void.class.equals(this.returnType); this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); this.returnsCursor = Cursor.class.equals(this.returnType); this.mapKey = getMapKey(method); this.returnsMap = this.mapKey != null; this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); this.paramNameResolver = new ParamNameResolver(configuration, method); } public Object convertArgsToSqlCommandParam(Object[] args) { return paramNameResolver.getNamedParams(args); } public boolean hasRowBounds() { return rowBoundsIndex != null; } public RowBounds extractRowBounds(Object[] args) { return hasRowBounds() ? (RowBounds) args[rowBoundsIndex] : null; } public boolean hasResultHandler() { return resultHandlerIndex != null; } public ResultHandler extractResultHandler(Object[] args) { return hasResultHandler() ? (ResultHandler) args[resultHandlerIndex] : null; } public String getMapKey() { return mapKey; } public Class<?> getReturnType() { return returnType; } public boolean returnsMany() { return returnsMany; } public boolean returnsMap() { return returnsMap; } public boolean returnsVoid() { return returnsVoid; } public boolean returnsCursor() { return returnsCursor; } private Integer getUniqueParamIndex(Method method, Class<?> paramType) { Integer index = null; final Class<?>[] argTypes = method.getParameterTypes(); for (int i = 0; i < argTypes.length; i++) { if (paramType.isAssignableFrom(argTypes[i])) { if (index == null) { index = i; } else { throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters"); } } } return index; } private String getMapKey(Method method) { String mapKey = null; if (Map.class.isAssignableFrom(method.getReturnType())) { final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class); if (mapKeyAnnotation != null) { mapKey = mapKeyAnnotation.value(); } } return mapKey; } } }
MapperMethod採用命令模式執行,根據上下文跳轉,它可能跳轉到許多方法中,我們不需要全部明白。我們可以看到裡面的exexuteForMany方法,再看看它的實現,實際上它最後就是通過sqlSession物件去執行物件的SQL。 至此,大家已經瞭解了MyBatis為什麼只用Mapper介面便能夠執行SQL,因為對映器的XML檔案的名稱空間對應的便是這個介面的全路徑,那麼它根據全路徑和方法名便能夠繫結起來,通過動態代理技術,讓這個介面跑起來。而後採用命令模式,最後還是使用SqlSession介面的方法使得它能夠執行查詢,有了這層封裝我們便可以使用介面程式設計,這樣程式設計就更簡單了。 2.SqlSession下的四大物件 我們已經知道了對映器其實就是一個動態代理物件,進入到了MapperMethod的execute方法。它經過簡單判斷就進入了SqlSession的刪除、更新、插入、選擇等方法,那麼這些方法如何執行呢?這是我們需要關注的問題,也是正確編寫外掛的根本。 顯然通過類名和方法名字匹配到我們配置的SQL,我們不需要去關心細節,我們關心的是設計框架。Mapper執行的過程是通過Executor、StatementHandler、ParameterHandler和ResultHandler來完成資料庫操作和結果返回。 (1)Executor代表執行器,由它來排程StatementHandler、ParameterHandler和ResultHandle等來執行對應的SQL; (2)StatementHandler的作用是使用資料庫的Statement(PreparedStatement)執行操作,它是四大物件的核心,起到承上啟下的作用; (3)ParameterHandler用於SQL對引數的處理; (4)ResultHandler是進行最後資料集(ResultSet)的封裝返回處理的;
3.SqlSession執行總結
SqlSession的執行原理十分重要,它是外掛的基礎,這裡我們對一次查詢胡總更新進行總結以加深對MyBatis內部執行的掌握。SqlSession內部執行圖,如圖所示:
SqlSession是通過Executor構建StatementHandler來執行的,而StatementHandler要經過下面三步。
(1)prepared預編譯SQL;
(2)parameterize設定引數;
(3)query/update執行SQL;
其中parameterize是呼叫parameterHandler的方法去設定的,而引數是根據型別處理器typeHandler去處理的。query/update方法是通過resultHandler進行處理結果的封裝,如果是update的語句,它就返回整數,否則它就通過typeHandler處理結果型別,然後用ObjectFactory提供的規則組裝物件,返回給呼叫者。這便是SqlSession執行的過程,我們清楚四大物件是如何運作的,同時也更好地理解了typeHandler和ObjectFactory在MyBatis中的應用。
小結: 本文主要參考了《深入淺出MyBatis技術原理與實戰》,希望能夠對大家有所幫助。