java如何獲取方法引數名
在java中,可以通過反射獲取到類、欄位、方法簽名等相關的資訊,像方法名、返回值型別、引數型別、泛型型別引數等,但是不能夠獲取方法的引數名。在實際開發場景中,有時需要根據方法的引數名做一些操作,比如像spring-mvc中,@RequestParam、@PathVariable註解,如果不指定相應的value屬性,預設就是使用方法的引數名做為HTTP請求的引數名,它是怎麼做到的呢?
在這樣情況下,有兩種方法獲取方法來解決這種需求,第一種方法是使用註解,在註解中指定對應應的引數名稱,在需要使用引數名稱時,獲取註解中相應的值即可。第二種方法是從位元組碼中獲取方法的引數名,但是這有一個限制,只有在編譯時使用了-g或-g:vars引數生成了除錯資訊,class檔案中才會生成方法引數名資訊(在本地變量表LocalVariableTable中),而使用-g:none方式編譯的class檔案中是沒有方法引數名資訊的。所以要想完全不依賴class檔案的編譯模式,就不能使用這種方式。下面討論一下兩方式的實現。
一、從註解中獲取
使用註解方式,我們需要自定義一個註解,在註解中指定引數名,然後通過反射機制,獲取方法引數上的註解,從而獲取到相應的註解資訊。這裡自定義的註解是Param,通過value引數指定引數名,定義了一個工具類ParameterNameUtils來獲取指定方法的引數名列表,這裡獲取測試類ParameterNameTest中定義的方法method1的引數名列表表,下面是具體的程式碼。
首先定義註解,這裡需要注意的是註解的目標是“引數”,保留策略是“執行時”,因為需要在執行時獲取註解資訊:
package com.mikan; import java.lang.annotation.*; /** * @author Mikan * @date 2015-08-04 23:39 */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Param { String value(); }
獲取註解中的引數名的工具類:
package com.mikan; import java.lang.annotation.Annotation; import java.lang.reflect.Method; /** * @author Mikan * @date 2015-08-05 00:26 */ public class ParameterNameUtils { /** * 獲取指定方法的引數名 * * @param method 要獲取引數名的方法 * @return 按引數順序排列的引數名列表 */ public static String[] getMethodParameterNamesByAnnotation(Method method) { Annotation[][] parameterAnnotations = method.getParameterAnnotations(); if (parameterAnnotations == null || parameterAnnotations.length == 0) { return null; } String[] parameterNames = new String[parameterAnnotations.length]; int i = 0; for (Annotation[] parameterAnnotation : parameterAnnotations) { for (Annotation annotation : parameterAnnotation) { if (annotation instanceof Param) { Param param = (Param) annotation; parameterNames[i++] = param.value(); } } } return parameterNames; } }
測試類:
package com.mikan;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* @author Mikan
* @date 2015-08-04 23:40
*/
public class ParameterNameTest {
public void method1(@Param("parameter1") String param1, @Param("parameter2") String param2) {
System.out.println(param1 + param2);
}
public static void main(String[] args) throws Exception {
Class<ParameterNameTest> clazz = ParameterNameTest.class;
Method method = clazz.getDeclaredMethod("method1", String.class, String.class);
String[] parameterNames = ParameterNameUtils.getMethodParameterNamesByAnnotation(method);
System.out.println(Arrays.toString(parameterNames));
}
}
輸出結果:
[parameter1, parameter2]
二、從class檔案中獲取
預設情況下,javac不會生成本地變量表資訊,只會生成行號表資訊,如果要生成本地變量表資訊,需要指定-g或-g:vars引數,如果要生成本地變量表和行號表資訊,可以使用-g:vars,lines引數。首先看一下使用-g引數和-g:none引數生成的class檔案的區別,再討論如何獲取。
測試類如下:
package com.mikan;
/**
* @author Mikan
* @date 2015-08-04 23:40
*/
public class ParameterNameTest1 {
public void method1(String param1, String param2) {
System.out.println(param1 + param2);
}
}
使用-g引數生成除錯資訊:
javac -g -d /Users/mikan/Documents/workspace/project/algorithm/target/classes /Users/mikan/Documents/workspace/project/algorithm/src/main/java/com/mikan/*.java
通過javap檢視位元組碼:
localhost:mikan mikan$ javap -c -v ParameterNameTest1.class
Classfile /Users/mikan/Documents/workspace/project/algorithm/target/classes/com/mikan/ParameterNameTest1.class
Last modified 2015-8-5; size 771 bytes
MD5 checksum 1c7617df9da5106249ab744feae684d1
Compiled from "ParameterNameTest1.java"
public class com.mikan.ParameterNameTest1
SourceFile: "ParameterNameTest1.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #27 // java/lang/StringBuilder
#4 = Methodref #3.#24 // java/lang/StringBuilder."<init>":()V
#5 = Methodref #3.#28 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#6 = Methodref #3.#29 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#7 = Methodref #30.#31 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #32 // com/mikan/ParameterNameTest1
#9 = Class #33 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/mikan/ParameterNameTest1;
#17 = Utf8 method1
#18 = Utf8 (Ljava/lang/String;Ljava/lang/String;)V
#19 = Utf8 param1
#20 = Utf8 Ljava/lang/String;
#21 = Utf8 param2
#22 = Utf8 SourceFile
#23 = Utf8 ParameterNameTest1.java
#24 = NameAndType #10:#11 // "<init>":()V
#25 = Class #34 // java/lang/System
#26 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#27 = Utf8 java/lang/StringBuilder
#28 = NameAndType #37:#38 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#29 = NameAndType #39:#40 // toString:()Ljava/lang/String;
#30 = Class #41 // java/io/PrintStream
#31 = NameAndType #42:#43 // println:(Ljava/lang/String;)V
#32 = Utf8 com/mikan/ParameterNameTest1
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 append
#38 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#39 = Utf8 toString
#40 = Utf8 ()Ljava/lang/String;
#41 = Utf8 java/io/PrintStream
#42 = Utf8 println
#43 = Utf8 (Ljava/lang/String;)V
{
public com.mikan.ParameterNameTest1();
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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/mikan/ParameterNameTest1;
public void method1(java.lang.String, java.lang.String);
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: aload_2
15: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
24: return
LineNumberTable:
line 10: 0
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 this Lcom/mikan/ParameterNameTest1;
0 25 1 param1 Ljava/lang/String;
0 25 2 param2 Ljava/lang/String;
}
localhost:mikan mikan$
關於位元組碼檔案的結構及各部分代表的含義,這裡就不一一說明了,如果有需要可以檢視相關的資料。
其中最後一部分,從public void method1(java.lang.String, java.lang.String);開始是方法method1的位元組碼資訊,可以從最後幾行看到:
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 this Lcom/mikan/ParameterNameTest1;
0 25 1 param1 Ljava/lang/String;
0 25 2 param2 Ljava/lang/String;
這幾行表示本地變量表資訊,可以看到method1方法有3個引數,為什麼會有3個引數呢?還記得在例項方法中我們可以使用this來表示當前例項麼,這就是為什麼,因為在編譯時編譯器自動給我們添加了一個引數代表當前例項,而且它是第一個引數,另外可以看到param1和param2,這就是方法宣告中的引數名。既然位元組碼中有方法引數名的資訊,那麼我們就可以通過某種方式從class檔案中獲取這些資訊。
另外還可以注意到,我們原始碼中System.out.println(param1 + param2);列印兩個字串引數連線,在編譯時編譯器自動給優化成了使用StringBuilder的方式,像這種類似的編譯器優化還有很多^_^。
下面來看一下不使用-g引數或使用-g:none引數編譯後的class檔案的格式,這裡只顯示方法method1的位元組碼,其他的省略:
localhost:mikan mikan$ javac -g:none -d /Users/mikan/Documents/workspace/project/algorithm/target/classes /Users/mikan/Documents/workspace/project/algorithm/src/main/java/com/mikan/*.java
localhost:mikan mikan$ javap -c -v ParameterNameTest1.class
public void method1(java.lang.String, java.lang.String);
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: aload_2
15: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
24: return
從生成的位元組碼可以看到,在使用-g:none引數後不再生成本地變量表,所以也就沒有方法的引數名等資訊,所以這種情況下就不能使用這種方式來獲取方法引數名了,只能使用前一種方法。
要從位元組碼中獲取方法的引數名資訊,就需要解析字碼碼檔案,這要求對位元組碼檔案很熟悉才行,這是一個很複雜的工作。還好我們可以使用第三方的類庫像asm、javassist來操作。這裡使用asm4.0。
程式碼如下:
package com.mikan;
import org.objectweb.asm.*;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
/**
* @author Mikan
* @date 2015-08-05 00:26
*/
public class ParameterNameUtils {
/**
* 獲取指定類指定方法的引數名
*
* @param clazz 要獲取引數名的方法所屬的類
* @param method 要獲取引數名的方法
* @return 按引數順序排列的引數名列表,如果沒有引數,則返回null
*/
public static String[] getMethodParameterNamesByAsm4(Class<?> clazz, final Method method) {
final Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes == null || parameterTypes.length == 0) {
return null;
}
final Type[] types = new Type[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
types[i] = Type.getType(parameterTypes[i]);
}
final String[] parameterNames = new String[parameterTypes.length];
String className = clazz.getName();
int lastDotIndex = className.lastIndexOf(".");
className = className.substring(lastDotIndex + 1) + ".class";
InputStream is = clazz.getResourceAsStream(className);
try {
ClassReader classReader = new ClassReader(is);
classReader.accept(new ClassVisitor(Opcodes.ASM4) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// 只處理指定的方法
Type[] argumentTypes = Type.getArgumentTypes(desc);
if (!method.getName().equals(name) || !Arrays.equals(argumentTypes, types)) {
return null;
}
return new MethodVisitor(Opcodes.ASM4) {
@Override
public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
// 靜態方法第一個引數就是方法的引數,如果是例項方法,第一個引數是this
if (Modifier.isStatic(method.getModifiers())) {
parameterNames[index] = name;
}
else if (index > 0) {
parameterNames[index - 1] = name;
}
}
};
}
}, 0);
} catch (IOException e) {
e.printStackTrace();
}
return parameterNames;
}
}
測試類:
package com.mikan;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* @author Mikan
* @date 2015-08-04 23:40
*/
public class ParameterNameTest {
public void method1(String param1, String param2) {
System.out.println(param1 + param2);
}
public static void main(String[] args) throws Exception {
Class<ParameterNameTest> clazz = ParameterNameTest.class;
Method method = clazz.getDeclaredMethod("method1", String.class, String.class);
String[] parameterNames = ParameterNameUtils.getMethodParameterNamesByAsm4(clazz, method);
System.out.println(Arrays.toString(parameterNames));
}
}
輸出結果:
[param1, param2]