天天看點

@param注解什麼意思_為什麼SpringMVC可以正确解析方法參數名稱,但MyBatis卻不行?...

@param注解什麼意思_為什麼SpringMVC可以正确解析方法參數名稱,但MyBatis卻不行?...

前語:

不要為了讀文章而讀文章,一定要帶着問題來讀文章,勤思考。
更多關于Java的技術和資訊可以關注我的專欄:【架構名人堂】 專欄免費給大家分享Java架構的學習資料和視訊
@param注解什麼意思_為什麼SpringMVC可以正确解析方法參數名稱,但MyBatis卻不行?...

發現問題

對Java位元組碼有一定了解的朋友應該知道,Java 在編譯的時候,預設不會保留方法參數名,是以我們無法在運作時擷取參數名稱。但是在使用 SpringMVC 的時候,我發現一個奇怪的現象:當我們需要接收請求參數的時候,相應的 Controller 方法隻需要正常聲明,就可以直接接收正确的參數,例如:

注:以下例子使用 maven 進行編譯,且非 SpringBoot 項目,SpringBoot 已經自動解決了參數名解析的問題,後面咱們會讨論
@RestController
@RequestMapping("calculator")
public class CalculatorController {

    @GetMapping("add")
    public int add(int aNum, int bNum) {
        return aNum + bNum;
    }
}
           

當接收到 /calculator/add?aNum=12&bNum=3 這樣的請求時,會傳回 15,即aNum 和 bNum 都能被正确解析。然而,當我們使用 MyBatis 時,如果接口方法有多個參數而且我們沒有打上 @Param 注解的話,執行的時候就會報錯。例如,我們有如下的接口:

@Mapper
public interface AccountMapper {
Account getByNameAndMobilePhone(String name, String mobilePhone);
}
           

方法中包含兩個參數,但是沒有打上 @Param 注解,這時候如果調用這個方法,會報錯:

org.apache.ibatis.binding.BindingException: Parameter ‘name’ not found.
Available parameters are [arg1, arg0, param1, param2]
           

從錯誤資訊中可以看出,是因為 MyBatis 沒有正确解析方法參數名稱導緻異常。這就很奇怪了,為什麼 Spring 可以正确解析方法參數名稱,但是 MyBatis 卻不行?Java編譯的時候預設會将方法參數名抹除,但我并沒有做特殊處理,Spring 又是從哪裡找到方法參數名的呢?帶着這些問題,我開始進行研究和探索。

擷取參數名的方式

通過查閱各種資料,我知道了擷取參數名稱的方式。

-g 參數

當我們對 Java 源碼進行編譯時,無論是直接使用指令行還是使用 IDE 為我們編譯,實際上最終都是調用 javac 指令進行的,在編譯的時候,我們如果添加上 -g 參數,即告訴編譯器,我們需要調試資訊,這時,生成的位元組碼當中就會包含局部變量表的資訊(方法參數也是局部變量),于是我們就可以通過解析位元組碼擷取參數名了。

我們用最最經典的 HelloWorld 程式中的 main 方法為例,看一下編譯的效果:

public class HelloWorld{

  public static void main(String[] argsName){
    System.out.println("HelloWorld!");
  }
}
           

我們直接執行如下 javac 指令來編譯并使用 javap 指令檢視生成的位元組碼資訊:

javac HelloWorld.java
javap -verbose HelloWorld.class
           
@param注解什麼意思_為什麼SpringMVC可以正确解析方法參數名稱,但MyBatis卻不行?...

可以看到,我們的參數名 argsName 已經被抹掉了。而如果位元組碼中都沒有我們所需要的資訊,那麼在運作時,反射或者是别的方法也都無能為力了,巧婦難為無米之炊呐。

接下來,我們試一下添加 -g 參數會發生什麼:

javac -g HelloWorld.java
javap -verbose HelloWorld.class
           
@param注解什麼意思_為什麼SpringMVC可以正确解析方法參數名稱,但MyBatis卻不行?...

可以看到,這裡多了一個 LocalVariableTable,即局部變量表,其中就有我們的參數名稱 argsName!那麼,我們如何在方法運作時從位元組碼資訊中擷取參數名稱呢?你可以直接通過 javap 來擷取位元組碼資訊,然後自己去根據資訊的格式去解析,然而這樣太低效了,而且太繁瑣了。

ASM 架構

這時候如果我們請大名鼎鼎的 ASM 來當“導遊”,帶着我們遊覽位元組碼内部構造,實作起來就輕松多了。

這個 ASM 可牛了,它不僅可以檢視位元組碼的資訊,甚至可以動态修改類的定義或者建立一個原本沒有的類!在各種架構中被廣泛地使用,SpringAOP中使用的 CGLib 底層就是使用 ASM 來實作的。

言歸正傳,如何通過 ASM 來擷取參數名稱呢? 直接上代碼:

<dependency>
  <groupId>asm</groupId>
  <artifactId>asm</artifactId>
  <version>3.3.1</version>
</dependency>
           

使用位元組碼工具ASM來擷取方法的參數名

public static String[] getMethodParamNames(final Method method) throws IOException {
 final int methodParameterCount =  method.getParameterTypes().length;
  final String[] methodParametersNames = new String[methodParameterCount];
  ClassReader cr = new ClassReader(method.getDeclaringClass().getName());
  ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
  cr.accept(new ClassAdapter(cw) {
  @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    
      MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
      final Type[] argTypes = Type.getArgumentTypes(desc);
      
      //參數類型不一緻
      if (!method.getName().equals(name) || !matchTypes(argTypes,  method.getParameterTypes())) {
      return mv;
      }
      
      return new MethodAdapter(mv) {
        @Override
        public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
        
          //如果是靜态方法,第一個參數就是方法參數,非靜态方法,則第一個參數是 this, 然後才是方法的參數
          int methodParameterIndex = Modifier.isStatic(method.getModifiers()) ? index : index - 1;
          if (0 <= methodParameterIndex && methodParameterIndex < methodParameterCount) {
            methodParametersNames[methodParameterIndex] = name;
          }
          super.visitLocalVariable(name, desc, signature, start, end, index);
          }
        };
      }
    }, 0);
    return methodParametersNames;
  }
較參數是否一緻
private static boolean matchTypes(Type[] types, Class<?>[] parameterTypes) {
if (types.length != parameterTypes.length) {
  return false;
}

for (int i = 0; i < types.length; i++) {
  if (!Type.getType(parameterTypes[i]).equals(types[i])) {
    return false;
  }
}
return true;
}
           

簡而言之,ASM使用了通路者模式,它就像一個導遊,帶着我們去遊覽位元組碼檔案中的各個“景點”。我們實作不同的 Visitor 接口就像是手上握有不同景點門票,導遊會帶着 ClassVisitor 去總體參觀類定義的景觀,而類内部有方法,如果你想看一下方法内部的定義,需要"額外購票",即需要實作 MethodVisitor 才能跟着導遊去參觀方法定義這個景點。而在遊覽各個景點的時候,我們可以隻遊覽我們感興趣的部分,這就可以繼承擴充卡(ClassAdapter和MethodAdapter分别是ClassVisitor和MethodVisitor的擴充卡)然後隻實作我們感興趣的方法即可。

這裡對于類的定義,我們隻對方法感興趣,是以隻實作 visitMethod 方法;在方法中,我們隻對 LocalVariableTable 有興趣,是以隻實作 visitLocalVariable 方法。這樣我們得到了局部變量表,再根據一些規則就可以拿到我們的參數名稱了!是不是很棒!

順便說一下,如果你使用 maven 來管理項目的話,這個 -g 參數會在編譯的時候自動加上,是以我們不需要額外添加就可以通過位元組碼拿到,這也就是為什麼 SpringMVC 可以拿到方法參數名稱的原因。

但是這種方式對于接口和抽象方法是不管用的,因為抽象方法沒有方法體,也就沒有局部變量,自然也就沒有局部變量表了:

@param注解什麼意思_為什麼SpringMVC可以正确解析方法參數名稱,但MyBatis卻不行?...

MyBatis 是通過接口跟 SQL 語句綁定然後生成代理類來實作的,是以它無法通過解析位元組碼來擷取方法參數名。

---------------------

版權聲明:本文為部落客原創文章,轉載請附上博文連結!