原創作品,可以轉載,但是請標注出處地址:http://www.cnblogs.com/V1haoge/p/6758456.html
1、回顧
之前解析了解析模塊parsing,其實所謂的解析模塊就是為了解析SQL腳本中的參數,根據給定的開始標記與結束標記來進行參數的定位獲取,然后由標記處理器進行參數處理,再然后將處理過后的參數再組裝回SQL腳本中。
如此一來,解析的目的就是為了處理參數。
這一篇看看binding綁定模塊。
2、binding模塊
binding模塊位於org.apache.ibatis.binding包下,這個模塊有四個類,這四個類是層層調用的關系,對外的是MapperRegistry,映射器注冊器。它會被Configuration類直接調用,用於將用戶自定義的映射器全部注冊到注冊器中,而這個注冊器顯而易見會保存在Configuration實例中備用(具體詳情后述)。
其實看到這個名稱,我們就會想起之前解析的類型別名注冊器與類型處理器注冊器,其實他們之間的目的差不多,就是注冊的內容不同罷了,映射器注冊器注冊的是MyBatis使用者自定義的各種映射器。
2.1 MapperRegistry:映射器注冊器
首先在注冊器中會定義一個集合用於保存注冊內容,這里是有HashMap:
1 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
上面的集合中鍵值的類型分別為Class類型與MapperProxyFactory類型,MapperProxyFactory是映射器代理工廠,通過這個工廠類可以獲取到對應的映射器代理類MapperProxy,這里只需要保存一個映射器的代理工廠,根據工廠就可以獲取到對應的映射器。
相應的,注冊器中必然定義了添加映射器和獲取映射器的方法來對外提供服務(供外部調取)。這里就是addMapper()方法與getMapper()方法,在該注冊器中還有一種根據包名來注冊映射器的方法addMappers()方法。因為該方法最后會調用addMapper()方法來完成具體的注冊功能,所以我們直接從addMappers()說起。
2.1.1 addMappers()
addMappers()方法是將給定包名下的所有映射器注冊到注冊器中。其源碼:
1 public <T> void addMapper(Class<T> type) { 2 //mapper必須是接口!才會添加 3 if (type.isInterface()) { 4 if (hasMapper(type)) { 5 //如果重復添加了,報錯 6 throw new BindingException("Type " + type + " is already known to the MapperRegistry."); 7 } 8 boolean loadCompleted = false; 9 try { 10 knownMappers.put(type, new MapperProxyFactory<T>(type)); 11 // It's important that the type is added before the parser is run 12 // otherwise the binding may automatically be attempted by the 13 // mapper parser. If the type is already known, it won't try. 14 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); 15 parser.parse(); 16 loadCompleted = true; 17 } finally { 18 //如果加載過程中出現異常需要再將這個mapper從mybatis中刪除,這種方式比較丑陋吧,難道是不得已而為之? 19 if (!loadCompleted) { 20 knownMappers.remove(type); 21 } 22 } 23 } 24 } 25 26 /** 27 * @since 3.2.2 28 */ 29 public void addMappers(String packageName, Class<?> superType) { 30 //查找包下所有是superType的類 31 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); 32 resolverUtil.find(new ResolverUtil.IsA(superType), packageName); 33 Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses(); 34 for (Class<?> mapperClass : mapperSet) { 35 addMapper(mapperClass); 36 } 37 } 38 39 /** 40 * @since 3.2.2 41 */ 42 //查找包下所有類 43 public void addMappers(String packageName) { 44 addMappers(packageName, Object.class); 45 }
其實這三個方法均可對外實現添加映射器的功能,分別應對不同的情況,其中addMappers(String packkageName)方法用於僅指定包名的情況下,掃描包下的每個映射器進行注冊;addMappers(String packageName, Class<?> superType)方法在之前的方法的前提下加了一個限制,必須指定超類,將包下滿足以superType為超類的映射器注冊到注冊器中;addMapper(Class<T> type)方法指的是將指定的類型的映射器添加到注冊器中。
按照介紹的順序排序為方法1、方法2、方法3,其中方法1通過調用方法2實現功能,方法2通過調用方法3實現功能,可見方法3為添加映射器的核心方法。
方法1調用方法2的時候添加了一個參數Object.class,表示所有的Java類都可以被注冊(因為Java類均是Object的子類)
方法2調用方法3的之前進行了掃描(使用ResolverUtil工具類完成包掃描),獲取滿足條件的所有類的set集合,然后在循環調用核心方法實現每個映射器的添加。
我們來看看核心方法addMapper(Class<T> type):
1、驗證要添加的映射器的類型是否是接口,如果不是接口則結束添加,如果是接口則執行下一步
2、驗證注冊器集合中是否已存在該注冊器(即重復注冊驗證),如果已存在則拋出綁定異常,否則執行下一步
3、定義一個boolean值,默認為false
4、執行HashMap集合的put方法,將該映射器注冊到注冊器中:以該接口類型為鍵,以以接口類型為參數調用MapperProxyFactory的構造器創建的映射器代理工廠為值
5、然后對使用注解方式實現的映射器進行注冊(一般不使用)
6、設置第三步的boolean值為true,表示注冊完成
7、在finally語句塊中對注冊失敗的類型進行清除
整個步驟很是簡單,重點就在第4步:在注冊器的集合中保存的是一個根據接口類型創建的映射器代理工廠實例,這個內容下一節解析
2.1.2 getMapper()
既然我們將映射器注冊到了注冊器中,當然是為了供外方使用的,要供外方使用,就必須提供獲取方法getMapper():
1 @SuppressWarnings("unchecked") 2 //返回代理類 3 public <T> T getMapper(Class<T> type, SqlSession sqlSession) { 4 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); 5 if (mapperProxyFactory == null) { 6 throw new BindingException("Type " + type + " is not known to the MapperRegistry."); 7 } 8 try { 9 return mapperProxyFactory.newInstance(sqlSession); 10 } catch (Exception e) { 11 throw new BindingException("Error getting mapper instance. Cause: " + e, e); 12 } 13 }
該方法很簡單,從集合中獲取指定接口類型的映射器代理工廠,然后使用這個代理工廠創建映射器代理實例並返回,那么我們就可以獲取到映射器的代理實例。
2.2 MapperProxyFactory:映射器代理工廠
這個類很簡單,直接上全碼:
1 package org.apache.ibatis.binding; 2 import java.lang.reflect.Method; 3 import java.lang.reflect.Proxy; 4 import java.util.Map; 5 import java.util.concurrent.ConcurrentHashMap; 6 import org.apache.ibatis.session.SqlSession; 7 /** 8 * 映射器代理工廠 9 */ 10 public class MapperProxyFactory<T> { 11 12 private final Class<T> mapperInterface; 13 private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); 14 15 public MapperProxyFactory(Class<T> mapperInterface) { 16 this.mapperInterface = mapperInterface; 17 } 18 19 public Class<T> getMapperInterface() { 20 return mapperInterface; 21 } 22 23 public Map<Method, MapperMethod> getMethodCache() { 24 return methodCache; 25 } 26 27 @SuppressWarnings("unchecked") 28 protected T newInstance(MapperProxy<T> mapperProxy) { 29 //用JDK自帶的動態代理生成映射器 30 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); 31 } 32 33 public T newInstance(SqlSession sqlSession) { 34 final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); 35 return newInstance(mapperProxy); 36 } 37 38 }
明顯是工廠模式,工廠模式的目的就是為了獲取目標類的實例,很明顯這個類就是為了在MapperRegisty中的addMapper(Class<T> type)方法的第4步中進行調用而設。
該類內部先調用MapperProxy的構造器生成MapperProxy映射器代理實例,然后以之使用JDK自帶的動態代理來生成映射器代理實例(代理的實現在下一節中)。
在這個代理工廠中定義了一個緩存集合,其實為了調用MapperProxy的構造器而設,這個緩存集合用於保存當前映射器中的映射方法的。
映射方法單獨定義,是因為這里並不存在一個真正的類和方法供調用,只是通過反射和代理的原理來實現的假的調用,映射方法是調用的最小單位(獨立個體),將映射方法定義之后,它就成為一個實實在在的存在,我們可以將調用過的方法保存到對應的映射器的緩存中,以供下次調用,避免每次調用相同的方法的時候都需要重新進行方法的生成。很明顯,方法的生成比較復雜,會消耗一定的時間,將其保存在緩存集合中備用,可以極大的解決這種時耗問題。
即使是在一般的項目中也會存在很多的映射器,這些映射器都要注冊到注冊器中,注冊器集合中的每個映射器中都保存着一個獨有的映射器代理工廠實例,而不是映射器實例,映射器實例只在需要的時候使用代理工廠進行創建,所以我們可以這么來看,MapperProxyFactory會存在多個實例,針對每個映射器有一個實例,這個實例就作為值保存在注冊器中,而下一節中的MapperProxy被MapperProxyFactory調用來生成代理實例,同樣也是與映射器接口一一對應的存在(即存在多個實例,只不過這個實例只會在需要的時候進行創建,不需要的時候是不存在的)。
2.3 MapperProxy:映射器代理
映射器代理類是MapperProxyFactory對應的目標類:
1 package org.apache.ibatis.binding; 2 import java.io.Serializable; 3 import java.lang.reflect.InvocationHandler; 4 import java.lang.reflect.Method; 5 import java.util.Map; 6 import org.apache.ibatis.reflection.ExceptionUtil; 7 import org.apache.ibatis.session.SqlSession; 8 /** 9 * 映射器代理,代理模式 10 * 11 */ 12 public class MapperProxy<T> implements InvocationHandler, Serializable { 13 14 private static final long serialVersionUID = -6424540398559729838L; 15 private final SqlSession sqlSession; 16 private final Class<T> mapperInterface; 17 private final Map<Method, MapperMethod> methodCache; 18 19 public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { 20 this.sqlSession = sqlSession; 21 this.mapperInterface = mapperInterface; 22 this.methodCache = methodCache; 23 } 24 25 @Override 26 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 27 //代理以后,所有Mapper的方法調用時,都會調用這個invoke方法 28 //並不是任何一個方法都需要執行調用代理對象進行執行,如果這個方法是Object中通用的方法(toString、hashCode等)無需執行 29 if (Object.class.equals(method.getDeclaringClass())) { 30 try { 31 return method.invoke(this, args); 32 } catch (Throwable t) { 33 throw ExceptionUtil.unwrapThrowable(t); 34 } 35 } 36 //這里優化了,去緩存中找MapperMethod 37 final MapperMethod mapperMethod = cachedMapperMethod(method); 38 //執行 39 return mapperMethod.execute(sqlSession, args); 40 } 41 42 //去緩存中找MapperMethod 43 private MapperMethod cachedMapperMethod(Method method) { 44 MapperMethod mapperMethod = methodCache.get(method); 45 if (mapperMethod == null) { 46 //找不到才去new 47 mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); 48 methodCache.put(method, mapperMethod); 49 } 50 return mapperMethod; 51 } 52 53 }
該類中的三個參數:
sqlSession:session會話
mapperInterface:映射器接口
methodCache:方法緩存
以上三個參數需要在構造器中進行賦值,首先session會話用於指明操作的來源,映射器接口指明操作的目標,方法緩存則用於保存具體的操作方法實例。在每個映射器代理中都存在以上三個參數,也就是說我們一旦我們使用過某個操作,那么這個操作過程中產生的代理實例將會一直存在,且具體操作方法會保存在這個代理實例的方法緩存中備用。
MapperProxy是使用JDK動態代理實現的代理功能,其重點就在invoke()方法中,首先過濾掉Object類的方法,然后從先從緩存中獲取指定的方法,如果緩存中不存在則新建一個MapperMethod實例並將其保存在緩存中,如果緩存中存在這個指定的方法實例,則直接獲取執行。
這里使用緩存進行流程優化,極大的提升了MyBatis的執行速率。
2.4 MapperMethod:映射器方法
映射器方法是最底層的被調用者,同時也是binding模塊中最復雜的內容。它是MyBatis中對SqlSession會話操作的封裝,那么這意味着什么呢?意味着這個類可以看做是整個MyBatis中數據庫操作的樞紐,所有的數據庫操作都需要經過它來得以實現。
我們單獨使用MyBatis時,有時會直接操作SqlSession會話進行數據庫操作,但是在SSM整合之后,這個數據庫操作卻是自動完成的,那么Sqlsession就需要被自動執行,那么組織執行它的就是這里的MapperMethod。
SqlSession會作為參數在從MapperRegisty中getMapper()中一直傳遞到MapperMethod中的execute()方法中,然后在這個方法中進行執行。
下面我們來解讀MapperMethod的源碼:
2.4.1 字段
1 private final SqlCommand command; 2 private final MethodSignature method;
這兩個字段類型都是以MapperMethod中的靜態內部類的方式定義的,分別表示sql命令與接口中的方法。
這兩個字段都是final修飾,表示不可變,即一旦賦值,就永遠是該值。
2.4.2 構造器
1 public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { 2 this.command = new SqlCommand(config, mapperInterface, method); 3 this.method = new MethodSignature(config, method); 4 }
使用構造器對上面的兩個字段進行賦值,這個賦值是永久的。
2.4.3 核心方法:execute()
1 //執行 2 public Object execute(SqlSession sqlSession, Object[] args) { 3 Object result; 4 //可以看到執行時就是4種情況,insert|update|delete|select,分別調用SqlSession的4大類方法 5 if (SqlCommandType.INSERT == command.getType()) { 6 Object param = method.convertArgsToSqlCommandParam(args); 7 result = rowCountResult(sqlSession.insert(command.getName(), param)); 8 } else if (SqlCommandType.UPDATE == command.getType()) { 9 Object param = method.convertArgsToSqlCommandParam(args); 10 result = rowCountResult(sqlSession.update(command.getName(), param)); 11 } else if (SqlCommandType.DELETE == command.getType()) { 12 Object param = method.convertArgsToSqlCommandParam(args); 13 result = rowCountResult(sqlSession.delete(command.getName(), param)); 14 } else if (SqlCommandType.SELECT == command.getType()) { 15 if (method.returnsVoid() && method.hasResultHandler()) { 16 //如果有結果處理器 17 executeWithResultHandler(sqlSession, args); 18 result = null; 19 } else if (method.returnsMany()) { 20 //如果結果有多條記錄 21 result = executeForMany(sqlSession, args); 22 } else if (method.returnsMap()) { 23 //如果結果是map 24 result = executeForMap(sqlSession, args); 25 } else { 26 //否則就是一條記錄 27 Object param = method.convertArgsToSqlCommandParam(args); 28 result = sqlSession.selectOne(command.getName(), param); 29 } 30 } else { 31 throw new BindingException("Unknown execution method for: " + command.getName()); 32 } 33 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { 34 throw new BindingException("Mapper method '" + command.getName() 35 + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); 36 } 37 return result; 38 }
這里可以很明顯的看出,在這個執行方法中封裝了對SqlSession的操作,而且將所有的數據庫操作分類為增刪改查四大類型,分別通過調用SqlSession的4大類方法來完成功能。
param表示的是SQL執行參數,可以通過靜態內部類MethodSignature的convertArgsToSqlCommandParam()方法獲取。
對SqlSession的增、刪、改操作使用了rowCountResult()方法進行封裝,這個方法對SQL操作的返回值類型進行驗證檢查,保證返回數據的安全。
針對SqlSession的查詢操作較為復雜,分為多種情況:
1、針對擁有結果處理器的情況:執行executeWithResultHandler(SqlSession sqlSession, Object[] args)方法
這種有結果處理器的情況,就不需要本方法進行結果處理,自然有指定的結果處理器來進行處理,所以其result返回值設置為null。
2、針對返回多條記錄的情況:執行executeForMany(SqlSession sqlSession, Object[] args)方法
內部調用SqlSession的selectList(String statement, Object parameter, RowBounds rowBounds)方法
針對Collections以及arrays進行支持,以解決#510BUG
3、針對返回Map的情況:執行executeForMap(SqlSession sqlSession, Object[] args)方法
內部調用SqlSession的selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds)方法
4、針對返回一條記錄的情況:執行SqlSession的selectOne(String statement, Object parameter)方法
針對前三種情況返回的都不是一條數據,在實際項目必然會出現需要分頁的情況,MyBatis中為我們提供了RowBounds來進行分頁設置,在需要進行分頁的情況,直接將設置好的分頁實例傳到SqlSession中即可。只不過這種方式的分頁屬於內存分頁,針對數據量小的情況比較適合,對於大數據量的查詢分頁並不適合,大型項目中的分頁也不會使用這種方式來實現分頁,而是采用之后會解析的分頁插件來時限物理分頁,即直接修改SQL腳本來實現查詢級的分頁,再不會有大量查詢數據占據內存。
下面將上面四種情況對應的方法源碼貼出:
1 //結果處理器 2 private void executeWithResultHandler(SqlSession sqlSession, Object[] args) { 3 MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName()); 4 if (void.class.equals(ms.getResultMaps().get(0).getType())) { 5 throw new BindingException("method " + command.getName() 6 + " needs either a @ResultMap annotation, a @ResultType annotation," 7 + " or a resultType attribute in XML so a ResultHandler can be used as a parameter."); 8 } 9 Object param = method.convertArgsToSqlCommandParam(args); 10 if (method.hasRowBounds()) { 11 RowBounds rowBounds = method.extractRowBounds(args); 12 sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args)); 13 } else { 14 sqlSession.select(command.getName(), param, method.extractResultHandler(args)); 15 } 16 } 17 18 //多條記錄 19 private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { 20 List<E> result; 21 Object param = method.convertArgsToSqlCommandParam(args); 22 //代入RowBounds 23 if (method.hasRowBounds()) { 24 RowBounds rowBounds = method.extractRowBounds(args); 25 result = sqlSession.<E>selectList(command.getName(), param, rowBounds); 26 } else { 27 result = sqlSession.<E>selectList(command.getName(), param); 28 } 29 // issue #510 Collections & arrays support 30 if (!method.getReturnType().isAssignableFrom(result.getClass())) { 31 if (method.getReturnType().isArray()) { 32 return convertToArray(result); 33 } else { 34 return convertToDeclaredCollection(sqlSession.getConfiguration(), result); 35 } 36 } 37 return result; 38 } 39 40 private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) { 41 Object collection = config.getObjectFactory().create(method.getReturnType()); 42 MetaObject metaObject = config.newMetaObject(collection); 43 metaObject.addAll(list); 44 return collection; 45 } 46 47 @SuppressWarnings("unchecked") 48 private <E> E[] convertToArray(List<E> list) { 49 E[] array = (E[]) Array.newInstance(method.getReturnType().getComponentType(), list.size()); 50 array = list.toArray(array); 51 return array; 52 } 53 54 private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) { 55 Map<K, V> result; 56 Object param = method.convertArgsToSqlCommandParam(args); 57 if (method.hasRowBounds()) { 58 RowBounds rowBounds = method.extractRowBounds(args); 59 result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey(), rowBounds); 60 } else { 61 result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey()); 62 } 63 return result; 64 }
2.4.4 靜態內部類:SqlCommand
1 //SQL命令,靜態內部類 2 public static class SqlCommand { 3 4 private final String name; 5 private final SqlCommandType type; 6 7 public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { 8 String statementName = mapperInterface.getName() + "." + method.getName(); 9 MappedStatement ms = null; 10 if (configuration.hasStatement(statementName)) { 11 ms = configuration.getMappedStatement(statementName); 12 } else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35 13 //如果不是這個mapper接口的方法,再去查父類 14 String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName(); 15 if (configuration.hasStatement(parentStatementName)) { 16 ms = configuration.getMappedStatement(parentStatementName); 17 } 18 } 19 if (ms == null) { 20 throw new BindingException("Invalid bound statement (not found): " + statementName); 21 } 22 name = ms.getId(); 23 type = ms.getSqlCommandType(); 24 if (type == SqlCommandType.UNKNOWN) { 25 throw new BindingException("Unknown execution method for: " + name); 26 } 27 } 28 29 public String getName() { 30 return name; 31 } 32 33 public SqlCommandType getType() { 34 return type; 35 } 36 }
這個內部類比較簡單,是對SQL命令的封裝,定義兩個字段,name和type,前者表示SQL命令的名稱,這個名稱就是接口的全限定名+方法名稱(中間以.連接),后者表示的是SQL命令的類型,無非增刪改查四種。
內部類中有一個帶參數的構造器用於對字段賦值,里面涉及到了MappedStatement類,這個類封裝的是映射語句的信息,在構建Configuration實例時會創建一個Map集合用於存儲所有的映射語句,而這些映射語句的解析存儲是在構建映射器的時候完成的(MapperBuilderAssistant類中)。
MappedStatement中的id字段表示的是statementName,即本內部類中的name值,所以在第22行代碼處直接將id賦值給name
2.4.5 靜態內部類:MethodSignature
首先我們來看看字段定義:
1 private final boolean returnsMany;//是否返回多個多條記錄 2 private final boolean returnsMap;//是否返回Map 3 private final boolean returnsVoid;//是否返回void 4 private final Class<?> returnType;//返回類型 5 private final String mapKey;//@mapKey注解指定的鍵值 6 private final Integer resultHandlerIndex;//結果處理器參數在方法參數列表中的位置下標 7 private final Integer rowBoundsIndex;//分頁配置參數在方法參數列表中的位置下標 8 private final SortedMap<Integer, String> params;//參數集合 9 private final boolean hasNamedParameters;//指定方法是否存在的參數注解
這些字段的含義正如源碼中注釋所述。
然后是帶參構造器:
1 public MethodSignature(Configuration configuration, Method method) { 2 this.returnType = method.getReturnType(); 3 this.returnsVoid = void.class.equals(this.returnType); 4 this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray()); 5 this.mapKey = getMapKey(method); 6 this.returnsMap = (this.mapKey != null); 7 this.hasNamedParameters = hasNamedParams(method); 8 //以下重復循環2遍調用getUniqueParamIndex,是不是降低效率了 9 //記下RowBounds是第幾個參數 10 this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); 11 //記下ResultHandler是第幾個參數 12 this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); 13 this.params = Collections.unmodifiableSortedMap(getParams(method, this.hasNamedParameters)); 14 }
構造器的參數分別為Configuration與Method,我們的字段的值部分是需要從這兩個參數中獲取的。
返回類型returnType從Method中獲取
hasNamedParameters表示是否存在注解方式定義的參數
獲取Method中RowBounds類型參數與ResultHandler類型參數的位置
下面是核心方法:convertArgsToSqlCommandParam(Object[] args):
1 public Object convertArgsToSqlCommandParam(Object[] args) { 2 final int paramCount = params.size(); 3 if (args == null || paramCount == 0) { 4 //如果沒參數 5 return null; 6 } else if (!hasNamedParameters && paramCount == 1) { 7 //如果只有一個參數 8 return args[params.keySet().iterator().next().intValue()]; 9 } else { 10 //否則,返回一個ParamMap,修改參數名,參數名就是其位置 11 final Map<String, Object> param = new ParamMap<Object>(); 12 int i = 0; 13 for (Map.Entry<Integer, String> entry : params.entrySet()) { 14 //1.先加一個#{0},#{1},#{2}...參數 15 param.put(entry.getValue(), args[entry.getKey().intValue()]); 16 // issue #71, add param names as param1, param2...but ensure backward compatibility 17 final String genericParamName = "param" + String.valueOf(i + 1); 18 if (!param.containsKey(genericParamName)) { 19 //2.再加一個#{param1},#{param2}...參數 20 //你可以傳遞多個參數給一個映射器方法。如果你這樣做了, 21 //默認情況下它們將會以它們在參數列表中的位置來命名,比如:#{param1},#{param2}等。 22 //如果你想改變參數的名稱(只在多參數情況下) ,那么你可以在參數上使用@Param(“paramName”)注解。 23 param.put(genericParamName, args[entry.getKey()]); 24 } 25 i++; 26 } 27 return param; 28 } 29 }
這個內部類是對映射器接口中的方法的封裝,其核心功能就是convertArgsToSqlCommandParam(Object[] args)方法,用於將方法中的參數轉換成為SQL腳本命令中的參數形式,其實就是將參數位置作為鍵,具體的參數作為值保存到一個Map集合中,這樣在SQL腳本命令中用鍵#{1}通過集合就能得到具體的參數。
之后會介紹,在params集合中保存的鍵值對分別為未排除RowBounds類型和ResultHandler類型的參數時各參數的位置下標與排除之后按原參數先后順序重新編號的新位置下標,而params的size大小其實是排除RowBounds類型和ResultHandler類型的參數之后參數的數量大小。
上面的源碼第14行,以params中的值(即新位置下標)為鍵,以通過原位置下標從方法的參數數組中獲取的具體參數為值保存在param集合中。之后再以paramn(n為從1開始的數值)為鍵,以通過原位置下標從方法的參數數組中獲取的具體參數為值保存在param集合中,這么一來,在param中就保存這兩份參數。
此處為了向老版本兼容,故在map中保存着兩份參數,一份是老版的與#{0}為鍵的參數另一種為以#{param1}為鍵的參數。
下面介紹params集合字段,這個集合用於保存方法中所有的參數,這是一個Map集合,通過getParams()方法來獲取方法中所有的參數,並將參數保存到Map集合中:
1 //得到所有參數 2 private SortedMap<Integer, String> getParams(Method method, boolean hasNamedParameters) { 3 //用一個TreeMap,這樣就保證還是按參數的先后順序 4 final SortedMap<Integer, String> params = new TreeMap<Integer, String>(); 5 final Class<?>[] argTypes = method.getParameterTypes(); 6 for (int i = 0; i < argTypes.length; i++) { 7 //是否不是RowBounds/ResultHandler類型的參數 8 if (!RowBounds.class.isAssignableFrom(argTypes[i]) && !ResultHandler.class.isAssignableFrom(argTypes[i])) { 9 //參數名字默認為0,1,2,這就是為什么xml里面可以用#{1}這樣的寫法來表示參數了 10 String paramName = String.valueOf(params.size()); 11 if (hasNamedParameters) { 12 //還可以用注解@Param來重命名參數 13 paramName = getParamNameFromAnnotation(method, i, paramName); 14 } 15 params.put(i, paramName); 16 } 17 } 18 return params; 19 } 20 21 private String getParamNameFromAnnotation(Method method, int i, String paramName) { 22 final Object[] paramAnnos = method.getParameterAnnotations()[i]; 23 for (Object paramAnno : paramAnnos) { 24 if (paramAnno instanceof Param) { 25 paramName = ((Param) paramAnno).value(); 26 } 27 } 28 return paramName; 29 }
這個方法主要額目的就是獲取指定方法Method中的所有排除RowBounds類型和ResultHandler類型的參數的參數,並將其重新排序,將順序保存到Map集合中,Map集合總保存的內容可能為:[(0,"0"),(1,"1"),(3,"2"),(4,"3"),(5,"4"),(6,"5")]的樣式,第一個數字(鍵)表示的是該參數在方法列表中的原位置(包含RowBounds類型和ResultHandler類型的參數),第二個數字(值字符串形式)表示的是該參數在排除RowBounds類型和ResultHandler類型的參數之后重排的順序,先后順序與之前一致。
暫時就解析到這里,這一篇將binding模塊進行了解析,重點就是MapperMethod類,這是一個樞紐類,是操作數據庫的必要一環。