代码中如何获得 Java 方法的形式参数名

对于一个 Java 方法 foo(int id, String name); 我们如何能在代码中获得形式参数名 id 和 name 呢?


我们知道通过反射 API Method.getGenericParameterTypes() 可以获得方法的参数类型,但是对于参数名一般就是 arg0, arg1, arg2 ..., 因为 Java 编译时把形式参数名擦除了。所以对完全擦除了形式参数名的字节码应该是没办法了,但我们自己写的类还是有能力去管控的。

对于自己写的类,有两种办法获得形式参数名,分别是

1) Java8 的 -parameters 编译参数,然后用 Java8 新引入的反射 API Parameter

我们先在 Java8 下运行下面的代码
 1package cc.unmi;
 2
 3import java.lang.reflect.Method;
 4import java.lang.reflect.Parameter;
 5
 6public class MethodParameterNames {
 7
 8  public static void main(String[] args) throws Exception{
 9    Method method = MethodParameterNames.class.getDeclaredMethod("foo", Integer.TYPE, String.class);
10    Parameter[] parameters = method.getParameters();
11    for(Parameter parameter: parameters) {
12      System.out.println(parameter.isNamePresent());
13      System.out.println(parameter.getType().getSimpleName() +": " + parameter.getName());
14    }
15  }
16
17  public void foo(int id, String name) {
18
19  }
20}

得到结果是

false
int: arg0
false
String: arg1

默认是也是得不到参数名的,isNamePresent() 是 false

如果编译的时候带上 -parameters 参数,再执行上面的代码结果就是

true
int: id
true
String: name

注:加不加编译参数 -parameters 产生的字节码用 javap -c 去反编译是看不出分别的,但是二进制码确实不一样的。看这里 Obtaining Names of Method Parameters. 现面一个对比,分别是没有加和加了 -parameters 编译出来的结果,直接用 vi 就能看出不同

上半部分是没有加 -parameters 编译参数产生的字节码,下半部是加了 -parameters 编译参数产生的字节码,注意看 foo() 方法部分,有 -parameters 则保留了形式参数名称在字节码中。

上面的对比结果同时也告诉了我 javap -c 并不是字节码的全部,javap -c 看到的相同也不意味着执行行为是一致的。

2) Play1 中的 play.utils.Java 的方法 public static String[] parameterNames(Method method)

这种方式值得参数一下,它用到了 javaassist  和 bytecodeparser 两个工具包,并依赖于 Play1 的编译方式,Play2 中已不适用。这里只列出它的源代码实现,已注释部分也在

摘自:play.utils.Java
 1 /**
 2  * Retrieve parameter names of a method
 3  */
 4public static String[] parameterNames(Method method) throws Exception {
 5    try {
 6        /*System.out.println("searching for " + "$" + method.getName() + LVEnhancer.computeMethodHash(method.getParameterTypes()));
 7        for(Field f : method.getDeclaringClass().getDeclaredFields()) {
 8            System.out.println(f.getName() + " : " + Modifier.toString(f.getModifiers()));
 9        }
10        for(Field f : method.getDeclaringClass().getFields()) {
11            System.out.println(f.getName() + " : " + Modifier.toString(f.getModifiers()));
12        }*/
13        return (String[]) method.getDeclaringClass().getDeclaredField("$" + method.getName() + LVEnhancer.computeMethodHash(method.getParameterTypes())).get(null);
14    } catch (Exception e) {
15        throw new UnexpectedException("Cannot read parameter names for " + method, e);
16    }
17}

上面有个 play.classloading.enhancers.LVEnhancer, 大致看下用到的两个方法

摘自:play.classloading.enhancers.LVEnhancer
 1public static Integer computeMethodHash(Class<?>[] parameters) {
 2    String[] names = new String[parameters.length];
 3    for (int i = 0; i < parameters.length; i++) {
 4        Class<?> param = parameters[i];
 5        names[i] = "";
 6        if (param.isArray()) {
 7            int level = 1;
 8            param = param.getComponentType();
 9            // Array of array
10            while (param.isArray()) {
11                level++;
12                param = param.getComponentType();
13            }
14            names[i] = param.getName();
15            for (int j = 0; j < level; j++) {
16                names[i] += "[]";
17            }
18        } else {
19            names[i] = param.getName();
20        }
21    }
22    return computeMethodHash(names);
23}
24
25public static Integer computeMethodHash(String[] parameters) {
26    StringBuffer buffer = new StringBuffer();
27    for (String param : parameters) {
28        buffer.append(param);
29    }
30    Integer hash = buffer.toString().hashCode();
31    if (hash < 0) {
32        return -hash;
33    }
34    return hash;
35}

其他:在 Play1 的世界里因为接管了编译器的某些活,所以它似乎还有办法获得实际传入方法的变量名,如在调用方法 foo() 时使用

int someId = 123;
String someName = "http://unmi.cc";
foo(someId, someName)

在代码中可以把 someId 和  someName 这两个名字得到

因为依据 Play1 对模板的调用

Detail orderDetail = ....
renderTemplate("details.html", orderDetail);

到模板文件 details.html 里就能用 ${orderDetail.id} 那样来访问 orderDetail 了,Java 代码中的变量名 orderDetail 和模板中的 orderDetail 必须对应的。


在 Play2 中开始使用 ParaNamer 来得到方法参数名称,见 https://github.com/paul-hammant/paranamer

更有趣的是编译时用 javac -g:vars TestLocalVarNames.java,然后用  javap -l 可以查看到变量名列表 LocalVariableTable永久链接 https://yanbin.blog/retrieve-java-method-formal-parameter-names/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。