龙空技术网

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

Java码农之路 4125

前言:

今天朋友们对“java参数名称”都比较注意,看官们都想要剖析一些“java参数名称”的相关内容。那么小编同时在网摘上搜集了一些对于“java参数名称””的相关知识,希望朋友们能喜欢,各位老铁们一起来学习一下吧!

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

咱们先来看这样一个小的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输出个人信息的方法