获取方法参数名的3种方式介绍
虽然Java编译器默认情况下会抹去方法的参数名,但有上面介绍了字节码的相关知识可知,我们还是有方法来得到方法的参数名的。下面介绍3个方案,供以参考。
方法一:使用-parameters
最为简单直接的方式,Java8源生支持:直接从java.lang.reflect.Parameter就能获取到,形如这样:
public class MainTest2 { public static void main(String[] args) throws NoSuchMethodException { Method method = MainTest2.class.getMethod("testArgName", String.class, Integer.class); System.out.println("paramCount:" + method.getParameterCount()); for (Parameter parameter : method.getParameters()) { System.out.println(parameter.getType().getName() + "-->" + parameter.getName()); } } public String testArgName(String name, Integer age) { return null; } }
输出:
paramCount:2 java.lang.String-->name java.lang.Integer-->age
当然,它有两个最大的弊端:
- 必须Java8或以上(由于java8已经普及率非常高了,所以这个还好)
- 编译参数必须有-parameters(由于依赖编译参数,所以对迁移是不太友好的,这点比较致命)
指定-parameters编译参数的方式:
- 手动命令方式编译:javac -parameters XXX.java
- IDE(以Idea为例)编译:
3.Maven编译:通过编译插件指定,保证项目迁移的正确性(推荐)
<!-- 编译环境在1.8编译 且附加编译参数:-parameters--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <compilerArgs> <arg>-parameters</arg> </compilerArgs> <source>${java.version}</source> <target>${java.version}</target> <compilerVersion>${java.version}</compilerVersion> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin>
优点:简单方便
缺点:需要特别的指定-parameters,不太方便(当然使用maven编辑插件来指定是相对靠谱的方案且推荐使用)
方案二:使用-g + javap命令
如上例子可以使用javac -g编译后,再使用javap获取到字节码信息,然后自己根据信息的格式把参数名提取出来(自己做、自己做、自己做)
这无异于让你自己解析http协议一般,你愿意做吗???所以此办法虽为一种办法,但是显然不便采用
方案三:借助ASM(推荐)
说到ASM,小伙伴们至少对这个名字应该是不陌生的。它是一个Java字节码操控框架,它能被用来动态生成类或者增强既有类的功能,它能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
对于ASM来说,Java class被描述为一棵树;使用 “Visitor”模式(导游模式)遍历整个二进制结构;事件驱动的处理方式使得用户只需要关注于对其编程有意义的部分(比如本文只关注方法参数,其它的不关心),而不必了解 Java 类文件格式的所有细节 。
ASM方式,它仍旧还是基于编译后的字节码做事的,正所谓巧妇难为无米之炊,所以它仍旧必须依赖于编译时的LocalVariableTable(-g 参数)。看这里~
你可能会发问:我使用idea编译/maven编译都没有自己去指定-g参数啊,为什么也好使呢?你的疑问同样也是我的疑问,我至今也还没弄清楚更根本的原因,但是我可以说如下两个现象:
- idea默认使用的是javac编译器,编译出来的字节码是带有LocalVariableTable的。但你也可以关闭它,如下图:
2.maven默认使用的也是javac编译,字节码也带有LocalVariableTable
(但是maven编译时候的编译命令、参数等,我无法获知。恳请精通maven的同学指点~)
小插曲:关于代理的科普(Proxy、CGLIB、Javassist、ASM ):
- ASM:Java字节码开源操控框架。操纵的级别是底层JVM的汇编指令级别,这就要求使用者对class组织结构和JVM汇编指令有一定的了解,要求颇高。
- Javassist:效果同上。相较于ASM它的特点是操作简单,并且速度还可以(当然没有ASM快)。重要的是:它并不要求你了解JVM指令/汇编指令~
- Proxy动态代理:动态生成(非提前编译好)代理类:$Proxy0 extends Proxy implements MyInterface{ ... },这就决定了它只能对接口(或者实现接口的类)进行代理,单继承机制也决定了它不能对(抽象)类进行代理~
- CGLIB:是一个基于ASM的强大的,高性能,高质量的字节码生成库。它可以在运行期扩展Java类与实现Java接口。
Spring AOP以及Hibernate对代理对象的创建中都使用了CGLIB
前面文章有介绍过了直接使用CGLIB的API来操作字节码/生成代理对象,本文将简单演示一下直接使用ASM框架来操作的示例
ASM使用示例
首先导入asm依赖包:
<!-- https://mvnrepository.com/artifact/asm/asm --> <dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.3.1</version> </dependency>
说明:asm现已升级到7.x版本了,并且GAV已变化。由于我对3.x熟悉点,所以此处我还是守旧吧~
基于ASM
提供工具方法getMethodParamNames(Method)
,获取到任何一个Method
的入参名称:
public class MainTest2 { // 拿到指定的Method的入参名们(返回数组,按照顺序返回) public static String[] getMethodParamNames(Method method) throws IOException { String methodName = method.getName(); Class<?>[] methodParameterTypes = method.getParameterTypes(); int methodParameterCount = methodParameterTypes.length; String className = method.getDeclaringClass().getName(); boolean isStatic = Modifier.isStatic(method.getModifiers()); String[] methodParametersNames = new String[methodParameterCount]; // 使用org.objectweb.asm.ClassReader来读取到此方法 ClassReader cr = new ClassReader(className); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); // 这一步是最红要的,开始visitor浏览了 // ClassAdapter是org.objectweb.asm.ClassVisitor的子类~~~~ 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); // 只visitor方法名相同和参数类型都相同的方法~~~ if (!methodName.equals(name) || !matchTypes(argTypes, methodParameterTypes)) { return mv; } // 构造一个MethodVisitor返回 重写我们关心的方法visitLocalVariable~~~ return new MethodAdapter(mv) { //特别注意:如果是静态方法,第一个参数就是方法参数,非静态方法,则第一个参数是 this ,然后才是方法的参数 @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { // 处理静态方法与否~~ int methodParameterIndex = isStatic ? 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; } }
运行案例:
public class MainTest2 { // 使用工具方法获取Method的入参名字~~~ public static void main(String[] args) throws SecurityException, NoSuchMethodException, IOException { Method method = MainTest2.class.getDeclaredMethod("testArgName", String.class, Integer.class); String[] methodParamNames = getMethodParamNames(method); // 打印输出 System.out.println(StringUtils.arrayToCommaDelimitedString(methodParamNames)); } private String testArgName(String name, Integer age) { return null; } }
输出:
name,age
效果复合预期,使用ASM拿到了我们期望的真实的方法参数名(没有指定任何编译参数哦)。使用基于ASM的方式,即使你是Java8以下的版本,都是能够正常获取到的,因为它并不依赖编译参数~~~