首页技术文章正文

SpringMVC可以正确解析方法参数名称,Mybatis不行【黑马java培训】

更新时间:2019年07月26日 10时45分58秒 来源:黑马程序员论坛

对Java字节码有一定了解的朋友应该知道,Java 在编译的时候,默认不会保留方法参数名,因此我们无法在运行时获取参数名称。但是在使用 SpringMVC 的时候,我发现一个奇怪的现象:当我们需要接收请求参数的时候,相应的 Controller 方法只需要正常声明,就可以直接接收正确的参数
[Java] 纯文本查看 复制代码
@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 注解的话,执行的时候就会报错。例如,我们有如下的接口:
[Java] 纯文本查看 复制代码
@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 方法为例,看一下编译的效果:

[Java] 纯文本查看 复制代码
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 来当“导游”,带着我们游览字节码内部构造,实现起来就轻松多了。
这个 ASM 可牛了,它不仅可以查看字节码的信息,甚至可以动态修改类的定义或者新建一个原本没有的类!在各种框架中被广泛地使用,SpringAOP中使用的 CGLib 底层就是使用 ASM 来实现的。有兴趣可以查看官网:https://asm.ow2.io/ 之前我也写过一篇文章《Java用ASM写一个HelloWorld程序》,有兴趣可以看一下。
言归正传,如何通过 ASM 来获取参数名称呢? 直接上代码:

[XML] 纯文本查看 复制代码
<dependency>
 <groupId>asm</groupId>
 <artifactId>asm</artifactId>
 <version>3.3.1</version>
</dependency>

[Java] 纯文本查看 复制代码
/**
* 使用字节码工具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 语句绑定然后生成代理类来实现的,因此它无法通过解析字节码来获取方法参数名。

推荐了解热门学科

java培训 Python人工智能 Web前端培训 PHP培训
区块链培训 影视制作培训 C++培训 产品经理培训
UI设计培训 新媒体培训 产品经理培训 Linux运维
大数据培训 智能机器人软件开发




传智播客是一家致力于培养高素质软件开发人才的科技公司“黑马程序员”是传智播客旗下高端IT教育品牌。自“黑马程序员”成立以来,教学研发团队一直致力于打造精品课程资源,不断在产、学、研3个层面创新自己的执教理念与教学方针,并集中“黑马程序员”的优势力量,针对性地出版了计算机系列教材50多册,制作教学视频数+套,发表各类技术文章数百篇。

传智播客从未停止思考

传智播客副总裁毕向东在2019IT培训行业变革大会提到,“传智播客意识到企业的用人需求已经从初级程序员升级到中高级程序员,具备多领域、多行业项目经验的人才成为企业用人的首选。”

中级程序员和初级程序员的差别在哪里?
项目经验。毕向东表示,“中级程序员和初级程序员最大的差别在于中级程序员比初级程序员多了三四年的工作经验,从而多出了更多的项目经验。“为此,传智播客研究院引进曾在知名IT企业如阿里、IBM就职的高级技术专家,集中研发面向中高级程序员的课程,用以满足企业用人需求,尽快补全IT行业所需的人才缺口。

何为中高级程序员课程?

传智播客进行了定义。中高级程序员课程,是在当前主流的初级程序员课程的基础上,增加多领域多行业的含金量项目,从技术的广度和深度上进行拓展“我们希望用5年的时间,打造上百个高含金量的项目,覆盖主流的32个行业。”传智播客课程研发总监于洋表示。




黑马程序员热门视频教程【点击播放】

Python入门教程完整版(懂中文就能学会) 零起点打开Java世界的大门
C++| 匠心之作 从0到1入门学编程 PHP|零基础入门开发者编程核心技术
Web前端入门教程_Web前端html+css+JavaScript 软件测试入门到精通


在线咨询 我要报名
和我们在线交谈!