如何通过反射获得方法的真实参数名(以及扩展研究)原创
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得有必要和大家分享交流一下。
咱们先来看这样一个小的demo:
这是一个很简单的小demo,里面就是一个简简单单的类Test1
,Test1
有一个包含两个参数的方法test
,在Test1
的main
方法中通过射来获得test
方法的所有参数的名字,并将其输出到标准流。我本以为这个demo的运行结果会得到方法的参数名,结果:
惊不惊喜,意不意外?和说好的不一样啊!
咱们先停一下,先把为什么反射没有拿到正确的值放到一边,先说说我为什么要研究“通过反射原理获得方法参数的实际名称”这件事呢:是因为我想仿照并实现Spring MVC中的“自动绑定”功能。大家知道Spring MVC里有一个“自动绑定”的功能,能够自动绑定请求参数的值到@RequestMapping
方法的参数上的,而不用任何额外的操作。
这个功能我觉得很方便,所以我想尝试自己仿造这个功能,然后用在公司的项目开发中。我猜测Spring是通过反射获得方法的参数名后根据参数名到request
中getParam(String name)
来获得实际的值然后绑定的。因此我就尝试着按照这个思路做,结果就遇到了上边提到的反射获得不了参数实际名称的问题。我将这个问题请教了老大,老大了解到我的意图后,经过验证,得出结论:Spring MVC能不能正常使用自动绑定是与java编译器编译时加不加-g
参数有关的,而这个-g
参数是代表着java编译器在编译时是否会输出调试信息。
其实也就是说:Spring是通过读取java编译器生成的调试信息从而获得的方法中参数的真实名称的。说到这里,这个问题基本也解决了,但是我还是想再多说一点我后续的学习结果。后续我研究了一下Spring对于方法参数这块的处理逻辑,也就是对于“自动绑定”功能的底层的实现。
那么,Spring 到底是用了什么“黑科技”来做到获得方法实际参数名的呢,咱们不妨就看Spring的源码吧,看看Spring到底是如何实现的。
Spring海量的源代码,从何看起呢,这里,我是这样解决的:我大体知道这个获得方法实际参数名的操作应当和Method
的getParameters()
方法有关,或者说它的方法里或许会调用到这个方法,那么好了,我们可以使用idea
提供的“查看调用栈”的功能,来顺藤摸瓜,看看在Spring中有没有调用到这个方法,如果有,那么解决方案应当就在调用方法的附近。
我们可以看到,果不其然,在调用栈里就有org.spring
包中的方法,其中有两个方法都是StandardReflectionParameterNameDiscoverer
类的方法,其实我们已经找到了,看这个类的名字就能知道,它是处理ParameterName
的Discoverer的(在这里我想再说点题外话,我个人非常赞同Spring这种全命名的编码风格,看到命名就能看明白这个类是在干什么,所以说代码应当是能“自描述”的)
好,我们再回到代码中来,继续看这个类:发现它有一段简要的注释:
大意就是这个类是针对使用了JDK8基于-parameters
编译参数的ParameterNameDiscoverer
的实现,这里这个-parameters
参数是怎么回事咱们先放一边,继续向上看StandardReflectionParameterNameDiscoverer
所实现的这个接口ParameterNameDiscoverer
,打开ParameterNameDiscoverer
这个接口,我们用idea的查看子类的功能,能够看到它一共有包括StandardReflectionParameterNameDiscoverer
在内的8
个子类
其中有一个名字里带“Default”的子类DefaultParameterNameDiscoverer
,按照一般套路来说,带Default的都是默认的实现,那么好了我们优先看它吧。
打开DefaultParameterNameDiscoverer
,我们发现,他做的大体就是通过判断standardReflectionAvailable
这个值来走向不同分支流程:一个是走向刚才提到的利用JDK8编译参数的StandardReflectionParameterNameDiscoverer
另一个是走向了LocalVariableTableParameterNameDiscoverer
好,现在又出现了熟悉的StandardReflectionParameterNameDiscoverer
了,那么我们返回去看吧,一会再看另一个分支的LocalVariableTableParameterNameDiscoverer
。
我们回到StandardReflectionParameterNameDiscoverer
中,再来看刚才那个-parameters
编译参数,这是个什么黑科技?既然他是个编译参数,那么咱们不妨试着用它编译一下咱们的代码试一下吧。
我们将idea
设置上-parameters
编译参数从新运行刚才的demo,发现这回的输出结果是:
已经能够拿到参数的真实名称了。那么,这个-parameters
到底是什么呢:我们可以来看一下oracle官方提供的javac文档:
通过文档可以看出加上这个参数后,编译器会生成元数据,从而使方法参数的反射能够拿到参数的信息。
这个功能是jdk8的新特性,我们就不仔细展开了,详情可以查看这两篇文档:
JDK 8 Features
JEP 118: Access to Parameter Names at Runtime
-parameters
这个黑科技咱们已经了解了,利用这个编译参数是可以获得方法参数的真实名称的,但是这个参数是jdk8之后才有的,那么之前的版本如何获得呢?我们继续看Spring源代码吧。现在我们来看另一个分支:LocalVariableTableParameterNameDiscoverer
,打开这个类:
其实看注释就明白了,这个LocalVariableTableParameterNameDiscoverer
是通过ASM library
分析LocalVariableTable
来实现获得参数实际名称的,ASM
是一个第三方的字节码操纵库,用这个库可以读取写入class文件,这个库有很广泛的应用,具体的我不展开介绍了。
我们重点说一下这个LocalVariableTable
吧,这个LocalVariableTable
是什么呢?我们不用文字来说明了,直接来看代码吧:
我们这次不看源文件了,来直接看编译后的class文件。用idea
打开Test1.class
:
然后在View
菜单中点选Show Bytecode
:
在弹出窗口中,我们可以看到,idea
以大纲的方式把class
文件的信息列了出来,而在其中就有LocalVariableTable
存在,而且在“LocalVariableTable”附近我们可以看到我们定义方法的参数的真实名称。现在我们也就明白了,对于8以下的jdk编译环境,Spring是使用ASM来读取class
文件中LocalVariableTable
信息从而获得参数真实名称的。
到此为止,我们已经基本了解了Spring中自动绑定背后的黑科技了。
这里我还想继续再多说一点,有关LocalVariableTable
和Java class
文件:class
文件可以说是Java实现跨平台特性的根本!不管在什么平台下,只要编译出来的class
文件符合规范,虚拟机就能够正常的执行。了解一下class
文件的相关知识其实对于理解各类class
文件操纵库以及基于class
操纵的AOP
等等编程模式的原理是很有帮助的,所以我们可以了解一下class
文件是什么样的结构的。想要了解class
文件的结构,最权威的莫过于官方的《Java虚拟机规范了》,在Java虚拟机规范中,第四章是有关class
文件结构的内容,我们可以大致过一遍。
通过阅读,我们可以大致了解到class
的结构:
A class file consists of a stream of 8-bit bytes. All 16-bit, 32-bit, and 64-bit
quantities are constructed by reading in two, four, and eight consecutive 8-bit
bytes, respectively. Multibyte data items are always stored in big-endian order,
where the high bytes come first. In the Java SE platform, this format is supported
by interfaces java.io.DataInput and java.io.DataOutput and classes such as
java.io.DataInputStream and java.io.DataOutputStream.
class文件可以用一个结构来表示:
这个结构中每一项大致的含义我们来简单说明一下吧(详情请查看虚拟机规范):
开头的magic
u4
叫做“魔数”,Java虚拟器通过读取这个数来判断当前文件是不是有效的u4
代表它是无符号
的4
个byte
,这个数始终应该是0xCAFEBABE
;
minor_version
、major_version
分别是class
文件的次版本
和主版本
;
u2
constant_pool_count
、cp_info
constant_pool[constant_pool_count-1]
代表常量池中项目数和代表了常量池本身;
u2
access_flags
: 代表class
访问标记,例如:public protected;
u2
this_class
: 代表放置类名在常量池中的索引;
u2
super_class
: 代表父类名称在常量池中的索引;
u2
interfaces_count
; u2
interfaces[interfaces_count]
; 代表所实现的接口集合的大小,及接口集合本身;
u2
fields_count
; field_info
fields[fields_count]
; 代表属性集合大小以及属性集合本身;
u2
methods_count
; method_info
methods[methods_count]
; 代表方法集合大小以及方法集合本身;
u2
attributes_count
; attribute_info
attributes[attributes_count]
; java class
文件内部属性信息集合大小和内部属性信息集合本身。这里提一下,我们前面的提到的LocalVariableTable
的信息就存储在这里。
到了这里我们大致回顾一下吧,我们从尝试解决反射获得方法参数真实名称开始,了解了Java编译参数、Spring自动绑定相关处理原理、jdk8编译参数新特性、以及Java class
文件的结构。通过这个过程,我们看到,就一个“自动绑定”这个平常都感觉不到它存在的小功能背后,还有这莫多深层次的技术在里面,由此可见,Spring之所以如此强大而且易用,离不开各类底层技术的支持,这就让我想起以前看到过的一位技术博主的标语:“只有深入,方能浅出”,想想确实是这个道理。
注:
在研究过程中我参考以下几位的文章,在此表示感谢:
深入理解JVM : class文件结构之类信息描述、字段表、方法表(2)