不要为了读文章而读文章,必定要带着问题来读文章,勤思考。java
更多关于Java的技术和资讯能够关注个人专栏:【架构名人堂】 专栏免费给你们分享Java架构的学习资料和视频
对Java字节码有必定了解的朋友应该知道,Java 在编译的时候,默认不会保留方法参数名,所以咱们没法在运行时获取参数名称。可是在使用 SpringMVC 的时候,我发现一个奇怪的现象:当咱们须要接收请求参数的时候,相应的 Controller 方法只须要正常声明,就能够直接接收正确的参数,例如:apache
注:如下例子使用 maven 进行编译,且非 SpringBoot 项目,SpringBoot 已经自动解决了参数名解析的问题,后面我们会讨论bash
@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 注解,这时候若是调用这个方法,会报错:app
org.apache.ibatis.binding.BindingException: Parameter ‘name’ not found.
Available parameters are [arg1, arg0, param1, param2]复制代码
从错误信息中能够看出,是由于 MyBatis 没有正确解析方法参数名称致使异常。这就很奇怪了,为何 Spring 能够正确解析方法参数名称,可是 MyBatis 却不行?Java编译的时候默认会将方法参数名抹除,但我并无作特殊处理,Spring 又是从哪里找到方法参数名的呢?带着这些问题,我开始进行研究和探索。框架
经过查阅各类资料,我知道了获取参数名称的方式。maven
-g 参数ide
当咱们对 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复制代码
能够看到,咱们的参数名 argsName 已经被抹掉了。而若是字节码中都没有咱们所须要的信息,那么在运行时,反射或者是别的方法也都无能为力了,巧妇难为无米之炊呐。
接下来,咱们试一下添加 -g 参数会发生什么:
javac -g HelloWorld.java
javap -verbose HelloWorld.class复制代码
能够看到,这里多了一个 LocalVariableTable,即局部变量表,其中就有咱们的参数名称 argsName!那么,咱们如何在方法运行时从字节码信息中获取参数名称呢?你能够直接经过 javap 来获取字节码信息,而后本身去根据信息的格式去解析,然而这样过低效了,并且太繁琐了。
这时候若是咱们请大名鼎鼎的 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 能够拿到方法参数名称的缘由。
可是这种方式对于接口和抽象方法是无论用的,由于抽象方法没有方法体,也就没有局部变量,天然也就没有局部变量表了:
MyBatis 是经过接口跟 SQL 语句绑定而后生成代理类来实现的,所以它没法经过解析字节码来获取方法参数名。
---------------------