原文: http://nullwy.me/2017/04/java...
若是以为个人文章对你有用,请随意赞扬
本文整理 Java 运行时获取方法参数名的两种方法,Java 8 的最新的方法和 Java 8 以前的方法。html
翻阅 Java 8 的新特性,能够看到有这么一条“JEP 118: Access to Parameter Names at Runtime”。这个特性就是为了能运行时获取参数名新加的。这个 JEP 只是功能加强的提案,并无最终实现的 JDK 相关的 API 的介绍。查看“Enhancements to the Reflection API” 会看到以下介绍:java
Enhancements in Java SE 8
Method Parameter Reflection: You can obtain the names of the formal parameters of any method or constructor with the method java.lang.reflect.Executable.getParameters. However,.class
files do not store formal parameter names by default. To store formal parameter names in a particular.class
file, and thus enable the Reflection API to retrieve formal parameter names, compile the source file with the-parameters
option of thejavac
compiler.
javac
文档中关于 -parameters
的介绍以下 [doc man ]:git
-parameters
Stores formal parameter names of constructors and methods in the generated class file so that the methodjava.lang.reflect.Executable.getParameters
from the Reflection API can retrieve them.
如今试验下这个特性。有以下两个文件:github
package com.test; public class TestClass { public int sum(int num1, int num2) { return num1 + num2; } }
package com.test; import java.lang.reflect.Method; import java.lang.reflect.Parameter; public class Java8Main { public static void main(String[] args) throws NoSuchMethodException { Method method = TestClass.class.getDeclaredMethod("sum", int.class, int.class); Parameter[] parameters = method.getParameters(); for (Parameter parameter : parameters) { System.out.println(parameter.getType().getName() + " " + parameter.getName()); } } }
先试试 javac
不加 -parameters
编译,结果以下:web
$ javac -d "target/classes" src/main/java/com/test/*.java $ java -cp "target/classes" com.test.Java8Main int arg0 int arg1
加上 -parameters
后,运行结果以下:spring
$ javac -d "target/classes" -parameters src/main/java/com/test/*.java $ java -cp "target/classes" com.test.Java8Main int num1 int num2
能够看到,加上 -parameters
后,正确得到了参数名。实际开发中,不多直接用命令行编译 Java 代码,项目通常都会用 maven 管理。在 maven 下,只需修改 pom 文件的 maven-compiler-plugin
插件配置便可,就是加上了 compilerArgs
节点 [doc ],以下:shell
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin>
“Enhancements in Java SE 8”提到,参数名信息回存储在 class 文件中。如今试试用 javap
( doc man)命令反编译生成的 class 文件。反编译 class 文件:apache
$ javap -v -cp "target/classes" com.test.TestClass Classfile /Users/yulewei/IdeaProjects/hellojava/target/classes/com/test/TestClass.class Last modified 2017-5-2; size 305 bytes MD5 checksum 24b99fec7f3062f5de1c3ca4270a1d36 Compiled from "TestClass.java" public class com.test.TestClass minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #3.#15 // java/lang/Object."<init>":()V #2 = Class #16 // com/test/TestClass #3 = Class #17 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 sum #9 = Utf8 (II)I #10 = Utf8 MethodParameters #11 = Utf8 num1 #12 = Utf8 num2 #13 = Utf8 SourceFile #14 = Utf8 TestClass.java #15 = NameAndType #4:#5 // "<init>":()V #16 = Utf8 com/test/TestClass #17 = Utf8 java/lang/Object { public com.test.TestClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public int sum(int, int); descriptor: (II)I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: iload_1 1: iload_2 2: iadd 3: ireturn LineNumberTable: line 6: 0 MethodParameters: Name Flags num1 num2 } SourceFile: "TestClass.java"
在结尾的 MethodParameters
属性就是,实现运行时获取方法参数的核心。这个属性是 Java 8 的 class 文件新加的,具体介绍能够参考官方“Java 虚拟机官方”文档的介绍,“4.7.24. The MethodParameters Attribute”,doc。c#
上文介绍了 Java 8 经过新增的反射 API 运行时获取方法参数名。那么在 Java 8 以前,有没有办法呢?或者在编译时没有开启 -parameters
参数,又如何动态获取方法参数名呢?其实 class 文件中保存的调试信息就能够包含方法参数名。api
javac
的 -g
选项能够在 class 文件中生成调试信息,官方文档介绍以下 [doc man ]:
-g
Generates all debugging information, including local variables. By default, only line number and source file information is generated.
-g:none
Does not generate any debugging information.
-g:[keyword list]
Generates only some kinds of debugging information, specified by a comma separated list of keywords. Valid keywords are:
source
Source file debugging information.
lines
Line number debugging information.
vars
Local variable debugging information.
能够看到默认是包含源代码信息和行号信息的。如今试验下不生成调试信息的状况:
$ javac -d "target/classes" src/main/java/com/test/*.java -g:none $ javap -v -cp "target/classes" com.test.TestClass Classfile /Users/yulewei/IdeaProjects/hellojava/target/classes/com/test/TestClass.class Last modified 2017-5-2; size 177 bytes MD5 checksum 559f5448154e4d7dd089f8155d8d0f55 public class com.test.TestClass minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #3.#9 // java/lang/Object."<init>":()V #2 = Class #10 // com/test/TestClass #3 = Class #11 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 sum #8 = Utf8 (II)I #9 = NameAndType #4:#5 // "<init>":()V #10 = Utf8 com/test/TestClass #11 = Utf8 java/lang/Object { public com.test.TestClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int sum(int, int); descriptor: (II)I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: iload_1 1: iload_2 2: iadd 3: ireturn }
对比上文的反编译结果,能够看到,输出结果中的 Compiled from "TestClass.java"
没了,Constant pool
中也再也不有 LineNumberTable
和 SourceFile
,code
属性里的 LocalVariableTable
属性也没了(固然,由于编译时没加 -parameters
参数,MethodParameters
属性天然也没了)。若选择不生成这两个属性,对程序运行产生的最主要的影响就是,当抛出异常时,堆栈中将不会显示出错代码所属的文件名和出错的行号,而且在调试程序的时候,也没法按照源码行来设置断点。
$ javac -d "target/classes" src/main/java/com/test/*.java -g:vars $ javap -v -cp "target/classes" com.test.TestClass Classfile /Users/yulewei/IdeaProjects/hellojava/target/classes/com/test/TestClass.class Last modified 2017-5-2; size 302 bytes MD5 checksum d430f817e0e2cfafc9095279c67aaa72 public class com.test.TestClass minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #3.#15 // java/lang/Object."<init>":()V #2 = Class #16 // com/test/TestClass #3 = Class #17 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LocalVariableTable #8 = Utf8 this #9 = Utf8 Lcom/test/TestClass; #10 = Utf8 sum #11 = Utf8 (II)I #12 = Utf8 num1 #13 = Utf8 I #14 = Utf8 num2 #15 = NameAndType #4:#5 // "<init>":()V #16 = Utf8 com/test/TestClass #17 = Utf8 java/lang/Object { public com.test.TestClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/test/TestClass; public int sum(int, int); descriptor: (II)I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: iload_1 1: iload_2 2: iadd 3: ireturn LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Lcom/test/TestClass; 0 4 1 num1 I 0 4 2 num2 I }
能够看到,code
属性里的出现了 LocalVariableTable
属性,这个属性保存的就是方法参数和方法内的本地变量。在演示代码的 sum
方法中没有定义本地变量,若存在的话,也将会保存在 LocalVariableTable
中。
javap
的 -v
选项会输出所有反编译信息,若只想看行号和本地变量信息,改用 -l
便可。输出结果以下:
$ javap -l -cp "target/classes" com.test.TestClass public class com.test.TestClass { public com.test.TestClass(); LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/test/TestClass; public int sum(int, int); LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Lcom/test/TestClass; 0 4 1 num1 I 0 4 2 num2 I }
若要所有生成所有提示信息,编译参数须要改成 -g:source,lines,vars
。通常在 IDE 下调试代码都须要调试信息,因此这三个参数默认都会开启。IDEA 下的 javac 默认参数设置,如图:
若使用 maven,maven 的默认的编译插件 maven-compiler-plugin
也会默认开启这三个参数 [doc],经实际验证也包括了LocalVariableTable
。
上文中讲了 class 文件中的调试信息中 LocalVariableTable
属性里就包含方法名参数,这就是运行时获取方法参数名的方法。读取这个属性,JDK 并无提供 API,只能借助第三方库解析 class 文件实现。
要解析 class 文件典型的工具库有 ObjectWeb 的 ASM(wiki,home,mvn,javadoc)、Apache 的 Commons BCEL(wiki,home,mvn,javadoc)、 日本教授开发的 Javassist(wiki,github,mvn,javadoc)等。其中 ASM 使用最广,使用 ASM 的知名开源项目有,AspectJ, CGLIB, Clojure, Groovy, JRuby, Jython, TopLink等等 [ref ]。固然使用 BCEL 的项目也不少 [ref ]。ASM 相对其余库的 jar 更小,运行速度更快 [javadoc ]。目前 asm-5.0.1.jar 文件大小 53 KB,BCEL 5.2 版本文件大小 520 KB,javassist-3.20.0-GA.jar 文件大小 751 KB。jar 包文件小,天然意味着代码量更少,提供的功能天然也少了。
先来看看用 BCEL 获取方法参数名的写法,代码以下:
package com.test; import org.apache.bcel.Repository; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.LocalVariable; import org.apache.bcel.classfile.LocalVariableTable; import org.apache.bcel.classfile.Method; import org.apache.bcel.generic.Type; public class BcelMain { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException { java.lang.reflect.Method m = TestClass.class.getDeclaredMethod("sum", int.class, int.class); JavaClass clazz = Repository.lookupClass("com.test.TestClass"); Method bcelMethod = clazz.getMethod(m); LocalVariableTable lvt = bcelMethod.getLocalVariableTable(); for (LocalVariable lv : lvt.getLocalVariableTable()) { System.out.println(lv.getName() + " " + lv.getSignature() + " " + Type.getReturnType(lv.getSignature())); } } }
输出结果:
this Lcom/test/TestClass; com.test.TestClass num1 I int num2 I int
ASM 的写法以下:
package com.test; import org.objectweb.asm.*; public class AsmMain { public static void main(String[] args) throws Exception { ClassReader classReader = new ClassReader("com.test.TestClass"); classReader.accept(new ParameterNameDiscoveringVisitor("sum", "(II)I"), 0); } private static class ParameterNameDiscoveringVisitor extends ClassVisitor { private final String methodName; private final String methodDesc; public ParameterNameDiscoveringVisitor(String name, String desc) { super(Opcodes.ASM5); this.methodName = name; this.methodDesc = desc; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (name.equals(this.methodName) && desc.equals(methodDesc)) return new LocalVariableTableVisitor(); return null; } } private static class LocalVariableTableVisitor extends MethodVisitor { public LocalVariableTableVisitor() { super(Opcodes.ASM5); } @Override public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) { System.out.println(name + " " + description); } } }
若使用 Spring 框架,对于运行时获取参数名,Spring 提供了内建支持,对应的实现类为 DefaultParameterNameDiscoverer
(javadoc)。该类先尝试用 Java 8 新的反射 API 获取方法参数名,若没法获取,则使用 ASM 库读取 class 文件的 LocalVariableTable
,对应的代码分别为 StandardReflectionParameterNameDiscoverer 和 LocalVariableTableParameterNameDiscoverer。