Java反射hasRealParameterData与isNamePresent方法未按预期工作
本问题是此前Stack Overflow问题《Preserving parameter/argument names in compiled java classes》的延续,原问题已有采纳答案,但给出的方案无法正常生效。
测试使用自行临时构建的JDK 19(已开放Executable.hasRealParameterData()方法的访问权限),测试代码如下:
public class Main { public static void main(String[] args) throws NoSuchMethodException { Method foo = Main.class.getMethod("foo", String.class, int.class); System.out.println(foo.hasRealParameterData()); } public void foo(String parameter1, int parameter2) {} }
第一轮测试:默认参数编译
首先直接使用默认命令编译:
% javac Main.java
运行编译后的类,控制台输出false,该结果符合预期。反编译后Main类代码如下,参数名是编译器生成的合成名称:
public class Main { public Main() {} public static void main(String[] var0) throws NoSuchMethodException { Method var1 = Main.class.getMethod("foo", String.class, Integer.TYPE); System.out.println(var1.hasRealParameterData()); } public void foo(String var1, int var2) {} // 参数名不是真实名称 }
第二轮测试:带局部变量调试信息编译
使用相同源码,改用添加局部变量调试信息的参数重新编译:
javac -g:vars Main.java
再次运行相同代码,控制台仍然输出false。此时反编译编译后的类已经可以看到真实参数名,结果不符合预期:
public class Main { public Main() {} public static void main(String[] args) throws NoSuchMethodException { Method foo = Main.class.getMethod("foo", String.class, Integer.TYPE); System.out.println(foo.hasRealParameterData()); } public void foo(String parameter1, int parameter2) {} // 参数名是真实名称 }
后续测试确认,就算编译时使用全量调试信息参数-g,运行结果仍然完全一致。
第三轮测试:使用公开标准API
不再调用JDK私有API,改用JDK自带公开标准APIParameter.isNamePresent()测试(该方法底层会调用Executable.hasRealParameterData()),测试代码如下:
public static void main(String[] args) throws NoSuchMethodException { Method foo = Main.class.getMethod("foo", String.class, int.class); Parameter parameter1 = foo.getParameters()[0]; Parameter parameter2 = foo.getParameters()[1]; System.out.println(parameter1.isNamePresent()); System.out.println(parameter2.isNamePresent()); } public void foo(String parameter1, int parameter2) {}
测试结果显示,无论使用何种编译参数,代码始终输出false false。
排查JDK源码发现,Executable.hasRealParameterData()会调用本地方法getParameters0(),其C++实现核心逻辑如下:
JVM_ENTRY(jobjectArray, JVM_GetMethodParameters(JNIEnv *env, jobject method)) { // method是java.lang.reflect.Method对象的句柄 Method* method_ptr = jvm_get_method_common(method); methodHandle mh (THREAD, method_ptr); Handle reflected_method (THREAD, JNIHandles::resolve_non_null(method)); const int num_params = mh->method_parameters_length(); if (num_params < 0) { // method_parameters_length返回-1代表不存在参数数据,直接返回null通知反射层 assert(num_params == -1, "num_params should be -1 if it is less than zero"); return (jobjectArray)NULL; } else { // 省略参数合法性校验、Parameter对象构造逻辑 // ... } } JVM_END
从源码逻辑可知,仅当不存在参数元数据时方法才会返回null,但编译后的class文件中明明已经存在参数名数据。
该现象属于JDK的bug,还是使用方式存在错误?
补充说明
- 即使在未经过修改的官方标准JDK上运行,
Parameter.isNamePresent()也存在上述异常表现。 - 反编译class文件可以看到真实参数名,但在IDEA中打断点调试时,
Parameter对象的name字段值却显示为arg0。
这是使用方式错误,不是JDK bug。
问题核心是混淆了两类完全独立的字节码存储结构:
-g系列参数控制生成的是**LocalVariableTable(局部变量表)**属性,这部分是给调试器用的,存储方法栈帧中局部变量(包括方法入参)的名称、槽位索引、生效范围信息。反编译工具能提取到参数名、IDEA调试时能看到变量值,都是读取的这个属性。- 反射API
Parameter.getName()、Parameter.isNamePresent()、Executable.hasRealParameterData()读取的是MethodParameters属性,这是Java 8之后专门为反射获取参数名设计的独立字节码属性,和局部变量表没有任何关联。
要生成供反射读取的MethodParameters属性,编译时需要添加的参数是-parameters,而非-g或者-g:vars。使用如下命令重新编译即可得到预期结果:
javac -parameters Main.java
编译完成后再运行测试代码,hasRealParameterData()会返回true,isNamePresent()也会返回true,调用Parameter.getName()就能拿到真实的parameter1、parameter2名称。
两套存储分离设计的原因很简单:局部变量表是调试附属信息,在代码混淆、字节码优化的场景下很容易被修改或移除,不适合作为反射API的稳定依赖;而MethodParameters是专门为反射场景设计的属性,只有显式开启-parameters参数时才会生成,稳定性更高。
内容的提问来源于stack exchange,提问作者Sergey Tsypanov

