本文整理 Java 运行时获取方法参数名的两种方法,Java 8 的最新的方法和 Java 8 之前的方法。
Java 8 的新特性 
翻阅 Java 8 的新特性 ,可以看到有这么一条“JEP 118 : Access to Parameter Names at Runtime”。这个特性就是为了能运行时获取参数名新加的。这个 JEP  只是功能增强的提案,并没有最终实现的 JDK 相关的 API 的介绍。查看“Enhancements to the Reflection API ” 会看到如下介绍:
 
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 the javac compiler.
 
javac 文档中关于 -parameters 的介绍如下 [ doc  man  ]:
-parameters  
Stores formal parameter names of constructors and methods in the generated class file so that the method java.lang.reflect.Executable.getParameters from the Reflection API can retrieve them.
 
现在试验下这个特性。有如下两个文件:
1 2 3 4 5 6 7 package  com.test;public  class  TestClass  {    public  int  sum (int  num1, int  num2)  {         return  num1 + num2;     } } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 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 编译,结果如下:
1 2 3 4 $  javac -d "target/classes"  src/main/java/com/test/*.java $  java -cp  "target/classes"  com.test.Java8Main int arg0 int arg1 
 
加上 -parameters 后,运行结果如下:
1 2 3 4 $  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  ],如下:
1 2 3 4 5 6 7 8 9 10 11 <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 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 $  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              # 2 = Class                  # 3 = Class                  # 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           # 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 。
class 文件中的调试信息 
上文介绍了 Java 8 通过新增的反射 API 运行时获取方法参数名。那么在 Java 8 之前,有没有办法呢?或者在编译时没有开启 -parameters 参数,又如何动态获取方法参数名呢?其实 class 文件中保存的调试信息就可以包含方法参数名。
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.
 
可以看到默认是包含源代码信息和行号信息的。现在试验下不生成调试信息的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 $  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              # 2 = Class                  # 3 = Class                  # 4 = Utf8               <init>    # 5 = Utf8               ()V    # 6 = Utf8               Code    # 7 = Utf8               sum     # 8 = Utf8               (II)I    # 9 = NameAndType           # 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 属性自然也没了)。若选择不生成这两个属性,对程序运行产生的最主要的影响就是,当抛出异常时,堆栈中将不会显示出错代码所属的文件名和出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 $  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              # 2 = Class                  # 3 = Class                  # 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           # 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 即可。输出结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $  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。
同样的,gradle 默认也会包含 调试信息 [ doc  ]。
代码如何实现 
上文中讲了 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 
先来看看用 BCEL 获取方法参数名的写法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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()));         }     } } 
 
输出结果:
1 2 3 this  Lcom/test/TestClass;  com.test.TestClass num1  I  int num2  I  int 
 
ASM 
ASM 的写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 框架,对于运行时获取参数名,Spring 提供了内建支持,对应的实现类为 DefaultParameterNameDiscoverer (javadoc )。该类先尝试用 Java 8 新的反射 API 获取方法参数名,若无法获取,则使用 ASM 库读取 class 文件的 LocalVariableTable,对应的代码分别为 StandardReflectionParameterNameDiscoverer  和 LocalVariableTableParameterNameDiscoverer 。
参考资料