Mybatis之方法如何映射到XML

Mybatis之方法如何映射到XML

前言

上文Mybatis之Mapper接口如何執行SQL中瞭解到,Mapper通過動態代理的方式執行SQL,但是並沒有詳細的介紹方法是如何做映射的,方法包括:方法名,返回值,參數等;這些都是如何同xxMapper.xml進行關聯的。

方法名映射

上文中提到緩存MapperMethod的目的是因為需要實例化SqlCommand和MethodSignature兩個類,而這兩個類實例化需要花費一些時間;而方法名的映射就在實例化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);
 }
 }
 }

首先獲取了方法的名稱和定義此方法的類,一般此類都是xxxMapper類;注此處的declaringClass類和mapperInterface是有區別的,主要是因為此方法可以是父類裡面的方法,而mapperInterface是子類;所以如果定義了父xxxMapper,同樣也能進行映射,所以可以看相關代碼:

 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;
 }
 }

可以看到方法名映射的時候並不是只有名稱,同樣在前面加了接口名稱類似:com
.xx.mapper.XXMapper+方法名,其對應的就是xxMapper.xml中的namespace+statementID;如果可以找到就直接返回configuration中的一個MappedStatement,暫時可以簡單為就是xxMapper.xml的一個標籤塊;如果找不到說明有可能在父類中,可以發現這裡使用遞歸,直到從所有父類中查找,如果還是找不到返回null;


接著上段代碼往下看,如果找不到對應的MappedStatement,會查看方法是否有@Flush註解,如果有指定命令類型為FLUSH,否則就拋出異常;找到MappedStatement從裡面獲取命令類型,所有類型包括:

public enum SqlCommandType {
 UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

INSERT, UPDATE, DELETE, SELECT其實也就是我們在xxMapper.xml中定義的標籤;

方法簽名

緩存的另一個對象是MethodSignature,直譯就是方法簽名,包含了方法的返回值,參數等,可以看其構造函數:

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);
 }

首先是獲取返回值的類型Type,我們在查詢的時候是需要更加具體的類型的,比如是對象類型,基本數據類型,數組類型,列表類型,Map類型,遊標類型,void類型;所以這裡獲取類型的時候需要Type類型的子接口類一共包括:ParameterizedType, TypeVariable, GenericArrayType, WildcardType這四種分別表示:
ParameterizedType:表示一種參數化的類型,比如Collection;
TypeVariable:表示泛型類型如T;
GenericArrayType:類型變量的數組類型;
WildcardType

:一種通配符類型表達式,比如?, ? extends Number;
獲取到具體類型之後,根據類型創建了四個標識:returnsMany,returnsMap,returnsVoid,returnsCursor,分別表示:
returnsMany:返回列表或者數組;
returnsMap:返回一個Map;
returnsVoid:沒有返回值;
returnsCursor:返回一個遊標;
除了以上介紹的幾個參數,還定義了三個參數分別是:RowBounds在所有參數中的位置,ResultHandler參數在所有參數中的位置,以及實例化了一個參數解析類ParamNameResolver用來作為參數轉為sql命令參數;注:RowBounds和ResultHandler是兩個特殊的參數,並不映射到xxMapper.xml中的參數,分別用來處理分頁和對結果進行再處理;

參數映射處理

參數的映射處理主要在ParamNameResolver中處理的,在實例化MethodSignature的同時,初始化了一個ParamNameResolver,構造函數如下:

private final SortedMap names;

private boolean hasParamAnnotation;

public ParamNameResolver(Configuration config, Method method) {
 final Class[] paramTypes = method.getParameterTypes();
 final Annotation[][] paramAnnotations = method.getParameterAnnotations();
 final SortedMap map = new TreeMap();
 int paramCount = paramAnnotations.length;
 // get names from @Param annotations
 for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
 if (isSpecialParameter(paramTypes[paramIndex])) {
 // skip special parameters
 continue;
 }
 String name = null;
 for (Annotation annotation : paramAnnotations[paramIndex]) {
 if (annotation instanceof Param) {
 hasParamAnnotation = true;
 name = ((Param) annotation).value();
 break;
 }
 }
 if (name == null) {
 // @Param was not specified.
 if (config.isUseActualParamName()) {
 name = getActualParamName(method, paramIndex);
 }
 if (name == null) {
 // use the parameter index as the name ("0", "1", ...)
 // gcode issue #71
 name = String.valueOf(map.size());
 }
 }
 map.put(paramIndex, name);
 }
 names = Collections.unmodifiableSortedMap(map);
 }

首先獲取了參數的類型,然後獲取參數的註解;接下來遍歷註解,在遍歷的過程中會檢查參數類型是否是RowBounds和ResultHandler,這兩個類型前文說過,是兩個特殊的類型並不用於參數映射,所以這裡過濾掉了,然後獲取註解中的值如@Param("id")中的value="id",如果沒有註解值,會檢查mybatis-config.xml中是否配置了

useActualParamName,這是一個開關表示是否使用真實的參數名稱,默認為開啟,如果關閉了開關則使用下標0,1,2...來表示名稱;下面已一個例子來說明一下,比如有如下方法:

public Blog selectBlog3(@Param("id") long id, @Param("author") String author);

那對應的names為:{0=id, 1=author},如果去掉@Param,如下:

public Blog selectBlog3(long id, String author);

對應的names為:{0=arg0, 1=arg1},如果關閉useActualParamName開關:

Mybatis之方法如何映射到XML

對應的names為:{0=0, 1=1};names這裡只是初始化信息,真正和xxMapper.xml中映射的參數還在ParamNameResolver中的另一個方法中做的處理:

public Object getNamedParams(Object[] args) {
 final int paramCount = names.size();
 if (args == null || paramCount == 0) {
 return null;
 } else if (!hasParamAnnotation && paramCount == 1) {
 return args[names.firstKey()];
 } else {
 final Map param = new ParamMap();
 int i = 0;
 for (Map.Entry entry : names.entrySet()) {
 param.put(entry.getValue(), args[entry.getKey()]);
 // add generic param names (param1, param2, ...)
 final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
 // ensure not to overwrite parameter named with @Param
 if (!names.containsValue(genericParamName)) {
 param.put(genericParamName, args[entry.getKey()]);
 }
 i++;
 }
 return param;
 }
 }

此方法會遍歷names,在遍歷之前會檢查是否為空和檢查是否只有一個參數,並且沒有給這個參數設置註解,那會直接返回這個參數值,並沒有key,這種情況xxMapper.xml對應的參數名稱其實是不關心的,什麼名稱都可以直接進行映射;如果不是以上兩種情況會遍歷names會分別往一個Map中寫入兩個key值,還是上面的三種情況,經過此方法處理後,值會發生如下變化:

{author=zhaohui, id=158, param1=158, param2=zhaohui}

以上是有設置註解名稱的情況;

{arg1=zhaohui, arg0=158, param1=158, param2=zhaohui}

以上是沒有設置註解名稱的情況,但是開啟了useActualParamName開關;

{0=158, 1=zhaohui, param1=158, param2=zhaohui}

以上是沒有設置註解名稱的情況,並且關閉了useActualParamName開關;

有了以上三種情況,所以我們在xxMapper.xml也會有不同的配置方式,下面根據以上三種情況看看在xxMapper.xml中如何配置:


第一種情況,xxMapper.xml可以這樣配置:

Mybatis之方法如何映射到XML

第二種情況,xxMapper.xml可以這樣配置:

Mybatis之方法如何映射到XML

第三種情況,xxMapper.xml可以這樣配置:

Mybatis之方法如何映射到XML

正是因為Mybatis在初始化參數映射的時候提供了多種key值,更加方便開發者靈活的設置值;雖然提供了多個key值選擇,但個人認為還是設置明確的註解更加規範;

總結

本文重點介紹了SqlCommand和MethodSignature這兩個類的實例化過程,SqlCommand重點介紹了方法名的映射通過接口路徑+方法名的方式和xxMapper.xml中的namespace+statementID進行映射,並且將到了遞歸父類的問題;然後就是方法簽名,通過方法的返回值類型創建了四個標識;最後講了參數的映射問題,Mybatis給開發者提供了多樣的映射key。

示例代碼地址

https://github.com/ksfzhaohui/blog/tree/master/mybatis


分享到:


相關文章: