为什么SpringMVC可以正确解析方法参数名称,但MyBatis却不行?

栏目: Java · 发布时间: 5年前

内容简介:对Java字节码有一定了解的朋友应该知道,Java 在编译的时候,默认不会保留方法参数名,因此我们无法在运行时获取参数名称。但是在使用 SpringMVC 的时候,我发现一个奇怪的现象:当我们需要接收请求参数的时候,相应的 Controller 方法只需要正常声明,就可以直接接收正确的参数,例如:

不要为了读文章而读文章,一定要带着问题来读文章,勤思考。

更多关于 Java 的技术和资讯可以关注我的专栏: 【架构名人堂】 专栏免费给大家分享Java架构的学习资料和视频

发现问题

对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复制代码

为什么SpringMVC可以正确解析方法参数名称,但MyBatis却不行?

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

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

javac -g HelloWorld.java
javap -verbose HelloWorld.class复制代码

为什么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 可以拿到方法参数名称的原因。

但是这种方式对于接口和抽象方法是不管用的,因为抽象方法没有方法体,也就没有局部变量,自然也就没有局部变量表了:

为什么SpringMVC可以正确解析方法参数名称,但MyBatis却不行?

MyBatis 是通过接口跟 SQL 语句绑定然后生成代理类来实现的,因此它无法通过解析字节码来获取方法参数名。

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

版权声明:本文为博主原创文章,转载请附上博文链接!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

An Introduction to Probability Theory and Its Applications

An Introduction to Probability Theory and Its Applications

William Feller / Wiley / 1991-1-1 / USD 120.00

Major changes in this edition include the substitution of probabilistic arguments for combinatorial artifices, and the addition of new sections on branching processes, Markov chains, and the De Moivre......一起来看看 《An Introduction to Probability Theory and Its Applications》 这本书的介绍吧!

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具