问题
在编写一个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里面的变量顺序跟程序中的顺序是不一致的,而jws里提供给外部调用的方法是直接取LocalVariableTable中前n个变量信息(n=参数个数,非static方法还会忽略掉第一个变量),自然返回的参数名就是错误的.
(但可以看到,返回的局部变量名是正确的,而不是var1之类的名称,说明class文件里是包含LocalVariableTable的)
至于为什么LocalVariableTable里的变量数据不是有序的,没有搜到确切原因(如果知道麻烦告知),但这种情况应该是正常的,因为:
可以搜到有人咨询这个问题
上述例子是本地通过官方jdk编译出来的class文件,jdk6~8结构都一致
(没理解错官方文档只说了LocalVariableTable这个Code Attribute的顺序是随意的,但没说里面的变量数据是否有序)
测试代码:
变量名顺序问题
用上述代码经过验证,ECJ编译出来的class文件里的LocalVariableTable是有序的,而eclipse和jws都使用了这个编辑器,而idea默认是javac,所以只在idea下会出现jws获取参数名不正确的问题.
正确获取参数名
LocalVariableTable本身是一个Code Attribute,其中**Start**,**Length**和**Slot**是计算的关键.
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然后排序就可以了)
问题解决方式
说了这么多,解决方式很简单:
修改获取方法参数名的算法,排序后再获取对应的参数名
把idea的编译器改成eclipse的,在[Preferences]->[Java Compiler]里的[Use
compiler],就变成和eclipse一样的编译结果了
|