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;
    }

}

测试类:

扫描二维码关注公众号,回复: 3115410 查看本文章

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]


猜你喜欢

转载自blog.csdn.net/mhmyqn/article/details/47294485
今日推荐