關於Mybatis的Mapper中多參數方法不使用@param註解報錯的問題

一、本文摘要

在使用低版本的Mybatis的時候,Mapper中的方法如果有多個參數時需要使用@param註解,才能在對應xml的sql語句中使用參數名稱獲取傳入方法的參數值,否則就會報錯。本文結合自身在真實開發環境中使用IDEA開發時遇到的問題來共同探討一下不使用@Param註解報錯背後的原因以及解決方案。

二、問題描述

最近使用IDEA進行開發,項目使用SpringBoot+Mybatis3.4.6,同樣的代碼檢出到本地IDEA後運行,在一個業務查詢模塊報錯,後臺打印日誌如下:
在這裏插入圖片描述
mybatis出現該錯誤的原因分析:我們正在調用一個具有多參數的mapper接口方法,對這個方法的調用其實是對mapper對應的xml中的一個sql的調用,並且我們在這個sql語句中使用#{方法參數名稱}的方式構建動態SQL,但是要想在sql語句中使用參數名稱獲取參數值那麼需要對mapper接口對應方法的每一個參數使用@Param註解,Param註解非常簡單,源代碼如下:

/**
 * @author Clinton Begin
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {
  String value();
}

它只有一個value屬性,這裏的value就等於mapper對應的xml文件中獲取參數值時要使用的key。於是我找到了對應報錯的代碼發現正是因爲多參數方法沒有使用@Param註解,在我加上該註解後便沒有錯誤了。
        到這裏事情看上去好像已經解決了,但是並沒有這麼簡單,我查看了很多mapper發現,有很多具有多個參數的mapper方法都沒有使用這個註解,按照這種修改方式,我豈不是要把幾乎所有的mapper都修改一遍,並且我是剛剛檢出的最新代碼,代碼不應該有問題纔對,於是詢問同事發現他們在自己的IDEA運行時並沒有我這個錯誤,所以說並不是@Param註解的問題。

三、尋求解決方案

同樣的代碼,在不同的機器上運行出現了不同的結果,那麼肯定有什麼不一樣的地方,首先JDK都一樣,系統環境也一樣,運行方式也一樣,下來就是運行環境IDEA,那麼IDEA是否有區別呢?詢問同事發現他們用的是比較新的版本2019.2.3,而我用的是2018.2.2版本,所以初步懷疑是IDEA的版本問題,但是好像按理來說不應該是IDEA的問題,真正運行JAVA字節碼的是本地的JRE環境,貌似和IDEA關係不大,但是這是目前唯一的線索,無論如何都要試一下。於是我下載了最新版本的IDEA,然後導入代碼,運行,結果發現竟然真的沒有報錯!這時候問題雖然解決了,但是爲什麼會這樣,背後的原因是什麼,和IDEA版本有什麼關係呢?這些問題如鯁在喉,讓我茶不思,飯不想…

四、尋找原因

當一個問題無法知道背後的真正原因時,那麼就算解決了也只是暫時的。爲了尋求真正的答案,我決定使用調試代碼的方式看一下mybatis執行查詢過程中是如何處理mapper接口方法的參數名稱的,最終找到了org.apache.ibatis.reflection.ParamNameResolver這個類,看類名就可以知道這是處理參數名稱的類,主要邏輯集中在它的構造方法:

  public ParamNameResolver(Configuration config, Method method) {
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
    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);
  }

接下來分析一下主要邏輯,首先看到的是需要獲取Param註解中的Value值:

String name = null;
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }

這裏的name變量就是後面構造動態sql時,用於獲取方法參數值的key,也就是你在xml文件中通過#{ }的方式獲取動態參數時的參數key。接下來看到的代碼是:

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

這裏可以看到再次判斷name是否爲null,如果爲null則判斷config.isUseActualParamName()是否爲true,如果是true則通過getActualParamName(method, paramIndex)方法獲取name,這些都執行完成如果name還是null,那麼就是最後的邏輯: name = String.valueOf(map.size());也就是說name等於當前方法參數的位置(“0”, “1”, …),源碼的註釋也說明了這一點:

use the parameter index as the name (“0”, “1”, …)

那麼getActualParamName(method, paramIndex)方法獲取name是什麼邏輯呢?接下來繼續看:
首先要進入這個方法的前提是config.isUseActualParamName()爲true:

public boolean isUseActualParamName() {
    return useActualParamName;
  }

config其實是mybatis的配置對象,這裏面的配置項目可以影響mybatis的行爲,具體配置項目可以從mybatis官方文檔查詢,這裏我們就看一下useActualParamName參數的含義,官方文檔 是這樣描述的:

設置名 描述 有效值 默認值
useActualParamName 允許使用方法簽名中的名稱作爲語句參數名稱。 爲了使用該特性,你的項目必須採用 Java 8 編譯,並且加上 -parameters 選項。(新增於 3.4.1) true 或者 false true

所以說這個屬性其實就是允許我們使用mapper接口方法的參數名稱當作sql語句的參數名稱,而且也不需要@Param註解,這個屬性默認是開啓的,使用這個特性還有以下幾個要求:

①採用 Java 8 編譯。
②編譯時加上-parameters 選項。
③mybatis在3.4.1以上

到這裏基本上可以確定真正的原因了,首先我和同事的JDK都是1.8,Mybatis的版本在文章開頭也說過了是3.4.6,所以只剩下-parameters選項,所以我懷疑是低版本的IDEA沒有這個選項,高版本的IDEA在編譯時可能默認加了這個選項。於是對比兩個版本的編譯設置如下:

①老版本(2018.2.2):
在這裏插入圖片描述
②新版本(2019.2.3):
在這裏插入圖片描述
果然如我們所料,新版本的IDEA編譯設置裏面默認添加了-parameters選項,所以在mybatis的配置項useActualParamName爲true的時候,對於多參數的mapper接口方法,可以不使用@Param註解,而在低版本的IDEA時並沒有添加這個選項,所以會出錯。

五、拓展延伸

在Java8之前,JAVA代碼編譯爲class文件後,方法參數的類型固定,但是參數名稱會丟失,所以當通過反射去獲取方法參數名稱的時候是不能夠得到原本源代碼中的參數名稱的,Java編譯器會丟掉這部分信息。從JDK1.8開始可以通過在編譯時添加-parameters這個選項來明確告訴編譯器我們需要保留方法參數的原本名稱。

那麼爲什麼不默認開啓這個選項呢?可能是爲了避免因爲保留參數名而導致class文件過大或者佔用更多的內存,又或者是有些參數可能會泄露安全信息吧。

最後我們親自來寫一段代碼驗證一下-parameters這個選項的作用:

public class Main {
    public static void main(String[] args) {
        Method[] methods = Main.class.getMethods();
        for (Method method:methods) {
            if ("parameterMethodTest".equals(method.getName())){
                Parameter[] parameters = method.getParameters();
                for (Parameter parameter:parameters) {
                    System.out.println(parameter.getName());
                }
            }
        }
    }
    public static void parameterMethodTest(int parameterOne,String parameterTwo,Object parameterThree){
        System.out.println("Hello World!");
    }
}

在以上這段代碼中,通過反射獲取parameterMethodTest的三個參數名稱並打印出來,首先我們在IDEA的編譯設置中去掉-parameters選項,運行結果如下:
在這裏插入圖片描述
可以看到這個時候參數名稱變成了arg0,arg1…
加上-parameters選項後,再運行結果如下:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章