Spring MVC和MyBatis作为当下最为流行的两个框架,大家平时开发中都在用。如果你往深了一步去思考,你应该会有这样的疑问:
- 在使用
Spring MVC的时候,即使不使用注解,只要参数名和请求参数的key对应上了,就能自动完成数值的封装 - 在使用
MyBatis(接口模式)时,接口方法向xml里的SQL语句传参时,必须使用@Param('')指定key值,在SQL中才可以取到。
为什么Spring MVC可以动态取到方法参数名称,而MyBatis的Mapper接口却无法支持?
问题发现
大家都知道,.java文件必须经过javac编译成.class文件才能被JVM执行。而在编译的时候,默认是不会保留方法参数名称的,取而代之的是arg0、arg1等表示。因此,想在运行时通过.class字节码直接拿到方法的参数名称是不可能做到的。
如下示例,很明显就是获取不到参数名称:
1 | public static void main(String[] args) throws NoSuchMethodException { |
打印内容为:
1 | 方法参数总数:2 |
但是在使用SpringMVC的时候,Controller的方法不用注解一样可以完成参数自动映射,例如:
1 | ("/test") |
ParameterNameDiscoverer
实际上,Spring底层是通过ParameterNameDiscoverer来实现参数名称获取的,它主要包含3个实现。
StandardReflectionParameterNameDiscoverer:基于JDK实现的参数名称发现器,必须在JDK8以上版本实现,并且只有在指定-parameters才能生效。LocalVariableTableParameterNameDiscoverer:基于ASM实现的参数名称发现器,通过ASM提供的通过字节码获取方法的参数名称,支持任何JDK版本。PrioritizedParameterNameDiscoverer:优先级参数名称发现器,可以认为是具体各个参数名称发现器的聚合管理,按优先级顺序取到一个可用的参数名称解析器进行使用。默认实现为DefaultParameterNameDiscoverer。
StandardReflectionParameterNameDiscoverer
StandardReflectionParameterNameDiscoverer实现非常简单,底层直接调用了JDK获取参数名称的方法。具体源码如下:
1 | public class StandardReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { |
需要特别注意的是,StandardReflectionParameterNameDiscoverer的使用条件有两个:
- 必须是
JDK8以上版本。 - 必须编译的时候有带上参数:
javac -parameters。
LocalVariableTableParameterNameDiscoverer
LocalVariableTableParameterNameDiscoverer是基于ASM实现的参数名称发现器,通过ASM提供的通过字节码获取方法的参数名称,支持任何JDK版本。
基本使用
1 | public class Main { |
输出结果如下:
1 | 方法:main 参数为:[args] |
实现原理
为了便于理解,先简单说说字节码中的两个概念:LocalVariableTable和LineNumberTable。
LineNumberTable
你是否曾经疑问过:线上程序抛出异常时显示的行号,为啥就恰好就是你源码的那一行呢?因为JVM执行的是.class文件,而该文件的行和.java源文件的行肯定是对应不上的,为何行号却能在.java文件里对应上?
其实底层就是LineNumberTable的作用:LineNumberTable属性存在于代码(字节码)属性中,它建立了字节码偏移量到源代码行号之间的联系。
LocalVariableTable
LocalVariableTable属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系。这个属性也是存在于代码(字节码)中。
从名字可以看出来:它是局部变量的一个集合,描述了局部变量和描述符以及和源代码的对应关系。
下面我使用javac和javap命令来演示一下这个情况:.java源码如下:
1 | package com.fsx.maintest; |
使用javac MainTest2.java编译成.class字节码,然后使用javap -verbose MainTest2.class查看该字节码信息如下:
从图中可看到,我红色标注出的行号和源码处完全一样,这就解答了我们上面的行号对应的疑问了:LineNumberTable它记录着在源代码处的行号。
Tips:此处并没有,并没有,并没有
LocalVariableTable。
源码不变,我使用javac -g MainTest2.java来编译,再看看对应的字节码信息如下(注意和上面的区别):
这里多了一个LocalVariableTable,即局部变量表,就记录着我们方法入参的形参名字。既然记录着了,这样我们就可以通过分析字节码信息来得到这个名称了。
javac的调试选项主要包含了三个子选项:
lines,source,vars。
如果不使用-g来编译,只保留源文件和行号信息;如果使用-g来编译那就都有了。
ASM是一个Java字节码操控框架,它能被用来动态生成类或者增强既有类的功能,它能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。借助于ASM,可以很轻松的修改.class字节码中的LocalVariableTable,从而实现动态获取参数名的功能。
原创不易,觉得文章写得不错的小伙伴,点个赞👍 鼓励一下吧~
欢迎关注我的开源项目:一款适用于SpringBoot的轻量级HTTP调用框架