如何通过反射获得方法的真实参数名(以及扩展研究)

·  阅读 1550
如何通过反射获得方法的真实参数名(以及扩展研究)

前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得有必要和大家分享交流一下。

咱们先来看这样一个小的demo:

这是一个很简单的小demo,里面就是一个简简单单的类Test1Test1有一个包含两个参数的方法test,在Test1main方法中通过射来获得test方法的所有参数的名字,并将其输出到标准流。我本以为这个demo的运行结果会得到方法的参数名,结果:

惊不惊喜,意不意外?和说好的不一样啊!

咱们先停一下,先把为什么反射没有拿到正确的值放到一边,先说说我为什么要研究“通过反射原理获得方法参数的实际名称”这件事呢:是因为我想仿照并实现Spring MVC中的“自动绑定”功能。大家知道Spring MVC里有一个“自动绑定”的功能,能够自动绑定请求参数的值到@RequestMapping方法的参数上的,而不用任何额外的操作。

这个功能我觉得很方便,所以我想尝试自己仿造这个功能,然后用在公司的项目开发中。我猜测Spring是通过反射获得方法的参数名后根据参数名到requestgetParam(String name)来获得实际的值然后绑定的。因此我就尝试着按照这个思路做,结果就遇到了上边提到的反射获得不了参数实际名称的问题。我将这个问题请教了老大,老大了解到我的意图后,经过验证,得出结论:Spring MVC能不能正常使用自动绑定是与java编译器编译时加不加-g参数有关的,而这个-g参数是代表着java编译器在编译时是否会输出调试信息。


其实也就是说:Spring是通过读取java编译器生成的调试信息从而获得的方法中参数的真实名称的。说到这里,这个问题基本也解决了,但是我还是想再多说一点我后续的学习结果。后续我研究了一下Spring对于方法参数这块的处理逻辑,也就是对于“自动绑定”功能的底层的实现。

那么,Spring 到底是用了什么“黑科技”来做到获得方法实际参数名的呢,咱们不妨就看Spring的源码吧,看看Spring到底是如何实现的。

Spring海量的源代码,从何看起呢,这里,我是这样解决的:我大体知道这个获得方法实际参数名的操作应当和MethodgetParameters()方法有关,或者说它的方法里或许会调用到这个方法,那么好了,我们可以使用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代表它是无符号4byte,这个数始终应该是0xCAFEBABE

minor_versionmajor_version分别是class文件的次版本主版本

u2 constant_pool_countcp_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之所以如此强大而且易用,离不开各类底层技术的支持,这就让我想起以前看到过的一位技术博主的标语:“只有深入,方能浅出”,想想确实是这个道理。


注:
在研究过程中我参考以下几位的文章,在此表示感谢:

反射获取一个方法中的参数名(不是类型)
Java 运行时获取方法参数名
java Class文件内部结构解析
深入理解JVM : class文件结构之常量池(1)
深入理解JVM : class文件结构之类信息描述、字段表、方法表(2)
触摸java常量池
实现一个Java Class解析器的实力代码分享
Java class file
Tutorial: Java Class file format, revealed…
The Java Class File Format
The Java class file lifestyle
An introduction to the basic structure and lifestyle of the Java class file

收藏成功!
已添加到「」, 点击更改