原文: 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。