对于一个 Java 方法 foo(int id, String name); 我们如何能在代码中获得形式参数名 id 和 name 呢?
我们知道通过反射 API Method.getGenericParameterTypes()
可以获得方法的参数类型,但是对于参数名一般就是 arg0, arg1, arg2 ..., 因为 Java 编译时把形式参数名擦除了。所以对完全擦除了形式参数名的字节码应该是没办法了,但我们自己写的类还是有能力去管控的。
对于自己写的类,有两种办法获得形式参数名,分别是
1) Java8 的 -parameters 编译参数,然后用 Java8 新引入的反射 API Parameter
我们先在 Java8 下运行下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package cc.unmi; import java.lang.reflect.Method; import java.lang.reflect.Parameter; public class MethodParameterNames { public static void main(String[] args) throws Exception{ Method method = MethodParameterNames.class.getDeclaredMethod("foo", Integer.TYPE, String.class); Parameter[] parameters = method.getParameters(); for(Parameter parameter: parameters) { System.out.println(parameter.isNamePresent()); System.out.println(parameter.getType().getSimpleName() +": " + parameter.getName()); } } public void foo(int id, String name) { } } |
得到结果是
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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * Retrieve parameter names of a method */ public static String[] parameterNames(Method method) throws Exception { try { /*System.out.println("searching for " + "$" + method.getName() + LVEnhancer.computeMethodHash(method.getParameterTypes())); for(Field f : method.getDeclaringClass().getDeclaredFields()) { System.out.println(f.getName() + " : " + Modifier.toString(f.getModifiers())); } for(Field f : method.getDeclaringClass().getFields()) { System.out.println(f.getName() + " : " + Modifier.toString(f.getModifiers())); }*/ return (String[]) method.getDeclaringClass().getDeclaredField("$" + method.getName() + LVEnhancer.computeMethodHash(method.getParameterTypes())).get(null); } catch (Exception e) { throw new UnexpectedException("Cannot read parameter names for " + method, e); } } |
上面有个 play.classloading.enhancers.LVEnhancer, 大致看下用到的两个方法
摘自:play.classloading.enhancers.LVEnhancer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public static Integer computeMethodHash(Class<?>[] parameters) { String[] names = new String[parameters.length]; for (int i = 0; i < parameters.length; i++) { Class<?> param = parameters[i]; names[i] = ""; if (param.isArray()) { int level = 1; param = param.getComponentType(); // Array of array while (param.isArray()) { level++; param = param.getComponentType(); } names[i] = param.getName(); for (int j = 0; j < level; j++) { names[i] += "[]"; } } else { names[i] = param.getName(); } } return computeMethodHash(names); } public static Integer computeMethodHash(String[] parameters) { StringBuffer buffer = new StringBuffer(); for (String param : parameters) { buffer.append(param); } Integer hash = buffer.toString().hashCode(); if (hash < 0) { return -hash; } return hash; } |
其他:在 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
。