面试题: 为何Mybatis获取不到接口的参数名?

点击蓝色“小哈学Java”关注我哟

加个“星标”,第一时间获取小哈推送的文章哦!

640?wx_fmt=jpeg

本文转载自公众号:程序员自学之道


发现问题

对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

640?wx_fmt=png可以看到,我们的参数名 argsName 已经被抹掉了。而如果字节码中都没有我们所需要的信息,那么在运行时,反射或者是别的方法也都无能为力了,巧妇难为无米之炊呐。

接下来,我们试一下添加 -g 参数会发生什么:

javac -g HelloWorld.java	
javap -verbose HelloWorld.class

640?wx_fmt=png可以看到,这里多了一个 LocalVariableTable,即局部变量表,其中就有我们的参数名称 argsName!

那么,我们如何在方法运行时从字节码信息中获取参数名称呢?你可以直接通过 javap 来获取字节码信息,然后自己去根据信息的格式去解析,然而这样太低效了,而且太繁琐了。

ASM 框架

这时候如果我们请大名鼎鼎的 ASM 来当“导游”,带着我们游览字节码内部构造,实现起来就轻松多了。

这个 ASM 可牛了,它不仅可以查看字节码的信息,甚至可以动态修改类的定义或者新建一个原本没有的类!在各种框架中被广泛地使用,SpringAOP中使用的 CGLib 底层就是使用 ASM 来实现的。有兴趣可以查看官网:https://asm.ow2.io/ 之前我也写过一篇文章《Java用ASM写一个HelloWorld程序》,有兴趣可以看一下。

言归正传,如何通过 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 可以拿到方法参数名称的原因。

但是这种方式对于接口和抽象方法是不管用的,因为抽象方法没有方法体,也就没有局部变量,自然也就没有局部变量表了: 640?wx_fmt=pngMyBatis 是通过接口跟 SQL 语句绑定然后生成代理类来实现的,因此它无法通过解析字节码来获取方法参数名。

虽然通过字节码的方法的确可以拿到参数名,但还是不方便,而且它对接口和抽象方法的参数名也无能为力。有没有更简便方式,不需要解析字节码又能支持抽象方法呢

其实是有的,请关注 程序员自学之道,咱们下一篇继续深入探索。

参考文献:

  • The Java Virtual Machine Specification

  • asm4-guide

  • 《spring mvc如何实现参数名绑定》 

    • https://dwz.cn/xDr5IhnK

  • 《使用ASM获得JAVA类方法参数名》 

    • https://dwz.cn/vwWIiipD

  • 《Java用ASM写一个HelloWorld程序》 

    • https://dwz.cn/L4MBXeIM

640?

更多推荐内容

↓↓↓

9 个可以快速掌握的 Java 性能调优技巧,你必须掌握!

面试题:你都知道哪些分布式Session实现方案?

我的天!e.printStackTrace() 导致内存锁死,你敢信?

面试题:说说看,Nginx 都能做些什么?

面试题:说说看,你是如何对SQL进行性能优化的?

一文教您如何通过 Docker 搭建 Ngnix,反向代理,并配置 Https 证书

面试官:大数据量sql分页查询很慢,有哪些优化方案?

面试题:谈谈 Redis 热点 Key 如何发现,解决方案有哪些

面试官:说说看,线上发生Mysql死锁,你的排查思路?

如果你喜欢本文

请长按二维码,关注小哈学Java

640?wx_fmt=jpeg

转发朋友圈,是对我最大的支持哟

文章有帮助的话,在看,转发吧。

是对小哈最大的支持哟 (*^__^*) 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值