Java反射获取方法参数名

问题

在编写一个jws(游戏中心的WEB框架)增强工具的时候,需要得到方法的参数名,而jws本身是可以获取参数名的(不然controller里将请求参数与方法参数绑定的功能也无法实现了).

但使用了jws提供的获取参数名方法时,却出现返回的参数名不正确的问题(只会出现在idea里面):

所以说:

  • 为什么可以获取方法参数?
  • 为什么eclipse和生产环境里不会发生这种问题?
  • 怎样可以正确获取方法的参数名?

问题排查

获取方法参数

众所周知,在java里面,直到java8才可以正式的通过反射获取方法参数名,而且还需要额外添加-parameters参数,官方理由是:

  • 参数名信息会使class文件变大,让处理消耗更多的资源
  • 容易被反编译,暴露敏感方法

所以正常来说,对java8以前的class文件进行反编译,方法参数名全部会变成var1,var2这样的东西(名字是反编译工具自己起的...).

但是某些时候,在一些WEB框架里,例如Spring MVC,JWS,却可以自动的将请求参数与对应的方法参数进行绑定.

是因为只要在编译工具javac里加上-g参数,就可以额外把本地变量名(参数也是其中一种)加到class文件中了.

javac中的debug信息

对于jvm来说,运行端代码,只需要有代码的字节码就可以了,根本不需要知道源码是什么样的.

我们之所以在ide里,可以对程序设置断点,可以对字节码反编译,是因为编译工具javac可以把一些源码相关的额外调试信息放到class文件里,具体通过-g参数控制(官方文档),可以添加的信息有:

  • 源码文件描述信息,**默认添加**(目测没什么用,就是多了一行'Compiled from ...')
  • 字节码与源码的行数的映射,即class文件里的LineNumberTable,**默认添加**,用于断点调试和异常栈(运行栈)中的代码行数
    • 没有的话在ide里无法设置断点调试,且抛出的异常栈中不会显示调用的代码行数,而是显示Unknown Source
  • 本地局部变量名表,即class文件里的LocalVariableTable,**默认不添加**,用户存放本地局部变量对应的变量名,包括参数名

所以,如果在编译时把局部变量信息放到了class文件里,运行时就可以通过字节码工具动态从class文件里拿到方法参数名了.

例子可以参考spring的org.springframework.core.LocalVariableTableParameterNameDiscoverer或以下文章.

debug信息的默认设置

在javac和ECJ(Eclipse Compiler for Java)里,调试信息的默认设置都是-g:lines,source

我们的工程用的框架之所以基本都能获取方法参数名,是因为:

  • jws:预编译功能通过ECJ实现,并且设置了生成LocalVariableTable
  • maven:compile插件默认生成所有debug信息(见设置文档),其他构建工具自行查找...
  • idea/eclipse:默认都生成所有debug信息

LocalVariableTable的结构

通过javap工具,可以看到方法里的LocalVariableTable是这个样子的:

    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         21       4     6   bbb   Ljava/lang/String;
         32       4     6   ccc   Ljava/lang/String;
          0      55     0  this   Lservices/api/server/TestApiService;
          0      55     1 test1   Ljava/lang/String;
          0      55     2 test2   Ljava/lang/String;
          5      50     3  test   Ljava/lang/Integer;
          9      46     4   aaa   Ljava/lang/String;
         12      43     5 isTest   Z

可以看到,LocalVariableTable里面的变量顺序跟程序中的顺序是不一致的,而jws里提供给外部调用的方法是直接取LocalVariableTable中前n个变量信息(n=参数个数,非static方法还会忽略掉第一个变量),自然返回的参数名就是错误的.

(但可以看到,返回的局部变量名是正确的,而不是var1之类的名称,说明class文件里是包含LocalVariableTable的)

至于为什么LocalVariableTable里的变量数据不是有序的,没有搜到确切原因(如果知道麻烦告知),但这种情况应该是正常的,因为:

  • 可以搜到有人咨询这个问题
  • 上述例子是本地通过官方jdk编译出来的class文件,jdk6~8结构都一致

(没理解错官方文档只说了LocalVariableTable这个Code Attribute的顺序是随意的,但没说里面的变量数据是否有序)

测试代码:

public class Test {
    public static String test(String roomId, String ucid) {
        Integer test = 1;
        String aaa = "a";
        System.out.println(test);
        boolean isTest = true;
        if (isTest) {
            String bbb = "bbb";
            aaa = "aaa";
        } else {
            String ccc = "ccc";
            aaa = "aa";
        }
        return roomId + ucid;
    }
}

变量名顺序问题

用上述代码经过验证,ECJ编译出来的class文件里的LocalVariableTable是有序的,而eclipse和jws都使用了这个编辑器,而idea默认是javac,所以只在idea下会出现jws获取参数名不正确的问题.

正确获取参数名

LocalVariableTable本身是一个Code Attribute,其中Start,LengthSlot是计算的关键.

java运行栈结构见文章中的75~91页.

  • Start:局部变量在方法中开始生效的偏移量,比如0就代表进入方法的时候就赋值,3代表执行到方法内的第3行命令才进行赋值
  • Length:局部变量生效范围的长度,比如在一个if语句里的局部变量,如果赋值偏移量是3,而if语句的结束偏移量是5,则Lenght为2
  • Slot:变量存放在局部变量区中的index,因为局部变量区中的空间可以复用(当一个变量失效后会被移除),所以此数字有可能重复

要正确获取对应的参数名,就需要对LocalVariableTable的数据进行排序,排序依据

  • 参数和this的start都是0,因为在方法执行前就会生效
  • slot是按顺序分配空间的,实例方法的第一个临时变量一定是this,所以如果有this则slot一定是0
  • 参数在方法上的顺序跟slot的排序结果一致,因为是按参数的顺序对参数赋值的

所以排序算法为:

先按Start排序,再按slot排序,根据实际情况看要不要去掉this(简单点start+slot然后排序就可以了)

问题解决方式

说了这么多,解决方式很简单:

  1. 修改获取方法参数名的算法,排序后再获取对应的参数名
  2. 把idea的编译器改成eclipse的,在[Preferences]->[Java Compiler]里的[Use compiler],就变成和eclipse一样的编译结果了

    (eclipse的编译器还有另外一些功能,见文章官方文档)

时间: 2024-08-30 11:26:06

Java反射获取方法参数名的相关文章

利用反射获取方法参数对象的泛型定义

问题描述 利用反射获取方法参数对象的泛型定义 public CommonRsp<AAARsp> hotelCheckAvailability(CommonReq<BBBReq> req) throws Exception; 如何通过反射获取如上方法中的参数泛型定义BBBReq? 解决方案 你这是java语法吗,如果是的话,其实java的泛型本质上是糖衣语法,编译后泛型是会被擦除的.你获取泛型本质上是没有意义的,应该是获取不到的.

Java获取代码中方法参数名信息的方法_java

前言 大家都知道随着java8的使用,在相应的方法签名中增加了新的对象Parameter,用于表示特定的参数信息,通过它的getName可以获取相应的参数名.即像在代码中编写的,如命名为username,那么在前台进行传参时,即不需要再编写如@Parameter("username")类的注解,而直接就能进行按名映射. 如下的代码参考所示: public class T { private interface T2 { void method(String username, Stri

java反射拼接方法名动态执行方法实例_java

近期由于负责项目的一个模块,该模块下有很多分类,每个分类都有一个编码code,这个值是作为一个参数携带过来的.但是每个code确实对应一个方法的. code的值有很多个,自己又不想做ifelse或者switch判断于是就狂搜资料,主要让我发现利用java的反射机制可以完美的解决这个问题 测试代码如下:(可以携带多个参数哦) package com.escs.xmlutils; import java.lang.reflect.Method; public class Test { public

Android Studio 3.0被调方法参数名提示的取消方法

android studio升级3.0之后, 代码中调用的方法增加了参数名提示: 很多人不习惯这个提示,我们来看看怎么取消这个提示: PS:下面看下Android Studio 3.0 gradle提示太老 解决方法 The android gradle plugin version 3.0.0-alpha1 is too old, please update to the latest version. To override this check from the command line

Java 反射获取类详细信息的常用方法总结_java

类ReflectionDemo 复制代码 代码如下: package Reflection; @Deprecated public class ReflectionDemo {     private String pri_field;     public String pub_field;     public ReflectionDemo(){}     public ReflectionDemo(String name){}     private ReflectionDemo(Stri

JAVA 反射获取返回值为MAP类型中的key

问题描述 publicMap<String,Object>getInfo(){Map<String,Object>data=newHashMap<String,Object>();data.put("a",1);returndata;} JAVA反射机制能获取到返回值MAP中的键a嘛?如果不行,有什么其它方法? 解决方案 解决方案二:这要用什么反射map的原生API就可以获取到KEY了解决方案三:Class<YourClass>clazz

java反射 获取参数为字符串数组的构造函数

问题描述 在研究反射的时候碰到一个问题如下:当我在A类中申明了一个参数为字符串数组(String[] strArray)的构造函数,并且在main方法中用getConstructor(String[].class) 或者 getConstructor(new Class[](String[].class))都无法获得相应的构造函数,然而,当我声明的是参数为整形数组(int[] intArray)的构造函数时,却可以获得相应的构造函数.谁能回答下嘛?十分感谢!代码如下://类Apublic cla

java反射改变方法的返回值

问题描述 java能在运行期间通过反射去改变方法的返回值么? 解决方案 如果这个值是类属性应该是可以改变的 因为反射是可以改变类属性的值 如果这个值是局部变量那应该是改变不了的下面的是通过改变类属性来间接改变方法的返回值public static void main(String[] args) throws Exception{User u=new User();u.setName("HH");Class cla=User.class;Method m=cla.getMethod(&

Java反射调用带参数的函数

目前本文中只考虑函数参数为基本类型.包装类型.String类型.其他负责类型,以后再慢慢补充.代码如下: package com.zkn.newlearn.reflect; import java.io.Serializable; /** * Created by zkn on 2016/5/9. */ public class ReflectTest02 implements Serializable{ public void testVariableArgument(String str,c