1. 程式人生 > >java如何獲取方法引數名

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]