概述
经过私下里的思考,对文章进行了更改
有时候我们需要通过获取方法的参数名称来完成一些业务需求,比如spring mvc 中controller中方法参数和http请求的参数进行映射。
springmvc中提供有@RequestParam和@PathVariable注解,通过注解给方法参数指定名称,在运行时可以通过反射获取到,这是比较简单的一种
方式,在springmvc中在没有使用注解的情况下,参数依然能够正确的映射,对此我是比较疑惑的,带着疑惑就进行了一番探究。
经过探究总结出获取方法参数名称的方式(见识浅,只知道这几种):
1.通过自定义注解的方式,再通过反射可获取:
相对来说实现简单,使用起来有诸多不变,如果方法参数很多,每一个都要加是比较繁琐的,可能有人觉得使用Map就方便了,但是使用Map使得
方法的可读性就变差了(不能清晰表明方法参数类型)。
2.jdk使用的1.8或以上的话,通过反射是可以直接获取方法参数名称的,此功能默认是关闭的,需要编译时开启(javac -parameters)
获取方式:
Parameter[] parameters = method.getParameters();
Parameter.getName()
3.相对以上两种,这种要复杂的多,需要对class文件结构非常熟悉,就是通过解析class文件中常量池,方法参数名信息是存储在MethodInfo结构中
code属性表中LocalVariableTable属性中的(这句话的描述可能不是很准确,要表达就是LocalVariableTable.好在有第三方比如asm这样的已
实现这样功能(有意者可以实操一下)。
但是此种方式并不能保证100%的获取到,什么情况下拿不到呢?(卖个关子-_-). 如果编译时有这个参数 ( javac -g:none ),class文件中就不会有
LocalVariableTable的信息。
(这个本人有实际操作,javac -g:none, 重新编译springmvc项目(controller未加注解),然后就无法调通controller方法)
这三种方式终归都是需要通过反射来获取的(插入一句:springmvc中如果参数是个pojo对象,http请求参数和其属性对应的话,其实只需要通过反射
解析其属性就可以了)
下面是给出在spring mvc中是如何处理HttpRequest中的参数和Controller中方法参数是如何映射的.
在spring中处方法参数的处理是通过 ParameterNameDiscoverer 接口来完成的,主要有以下两个实现:
StandardReflectionParameterNameDiscoverer: jdk8及以上版本使用,这里不贴代码,因为比较简单
LocalVariableTableParameterNameDiscoverer: jdk8以前版本使用
下面着重的说一下 LocalVariableTableParameterNameDiscoverer实现,了解过JVM读者的应该知道 【LocalVariableTable】 存储的便是
方法局部变量数据(确切的说表示的只是常量池的索引,最终的信息还是在常量池中)
LocalVariableTableParameterNameDiscoverer代码::
public String[] getParameterNames(Method method) {
Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); // 获取实际的方法,因为method可能是桥接方法
Class<?> declaringClass = originalMethod.getDeclaringClass(); // 获取声明方法的类
Map<Member, String[]> map = this.parameterNamesCache.get(declaringClass); // 查看缓存里是否存在
if (map == null) {
// 获取字节码,解析class文件中常量池,下方有解释
map = inspectClass(declaringClass);
this.parameterNamesCache.put(declaringClass, map);
}
if (map != NO_DEBUG_INFO_MAP) {
return map.get(originalMethod);
}
return null;
}
接着是【inspectClass()】方法代码片段::
InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz));
ClassReader classReader = new ClassReader(is);// asm
Map<Member, String[]> map = new ConcurrentHashMap<Member, String[]>(32);
// 作者粗鄙的注释(-_-):
// 解析常量池(constant_pool)之后的class文件信息,重点是method_info,其结构为:
// method_info {
// u2 access_flags;
// u2 name_index;
// u2 descriptor_index;
// u2 attributes_count;
// attribute_info attributes[attributes_count]; // 详细信息是在class文件最后一个数据结构[属性表]中
// }
//
// 属性表attribute_info
// 然后会解析[attribute_info],主要就是[Code_attribute]属性,然后是[Code_attribute]中[LocalVariableTable_attribute]
// [LocalVariableTable_attribute] 的结构就请读者自行查看虚拟机规范中所定义的.
// 之后就是到Code中code[] 属性,最后会索引到constant_pool
// 其实所有的解析最终都会到常量池(constant_pool),最终方法的参数名还在CONSTANT_Utf8_info中
// class文件结构,多种数据结构有嵌套引用,所以是比较复杂的
// 此方法之前有一步比较重要,就是要先解析常量池,说是解析常量池,最主要的目的跳过常量池,此方法是解析常量池之后的数据结构的.
// 常量池的解析是在【ClassReader】的构造器内完成的
classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0);
// something code...
接着我们在看下【ClassReader】的构造方法代码片段::
public ClassReader(final byte[] b, final int off, final int len) {
this.b = b; // 字节码
items = new int[readUnsignedShort(off + 8)];
// 代码解释:
// 注意:这里及下面的一些注释,仅供参考,如有错误之处,还请谅解,请以虚拟机规范为准
// readUnsignedShort(off + 8)解释如下: 读取索引 8,9两个位置的字节,是为常量池成员数
// class文件结构是固定的,有情趣请参考虚拟机规范Class文件结构,这里简单表示一下
// 前4个字节表示的是class文件的魔数:固定为0xCAFEBABE,紧接着的两个是次版本号,再紧接着两个字节是主版本号
// 主版本号之后的两个字节表示的常量池的大小(等于常量池表constant_pool中成员数加1)
// 紧随常量池的大小之后的便是 constant_pool表,其成员类型为cp_info{u1:tag,u1:info[]},u1表示1个字节大小,tag表示成员类型
// 更具体的信息请读者参考虚拟机规范
int n = items.length;
strings = new String[n];
int max = 0;
int index = off + 10; // 常量池开始位置
// 以下代码是为了确定常量池中CONSTANT_Utf8_info.length的最大长度,以及常量池结束后的索引位置也就是[access_flags数据结构]
// (占用两个字节)类型的数据,表示类的访问权限数据.
// 因为常量池表constant_pool中成员是变长的,因此需要解析常量池才能知道[access_flags]的索引位置,其目的就是跳过常量池这个数据
// 结构
// class文件结构是固定的,各类型的数据结构按照固定顺序,中间无人后分割的紧凑排列存储着,
// 这里主要的作用就是为了找出下一个类型的数据结构的索引位置(跳过常量池).
for (int i = 1; i < n; ++i) {
items[i] = index + 1;
int size;
switch (b[index]) {
case ClassWriter.FIELD:
case ClassWriter.METH:
case ClassWriter.IMETH:
case ClassWriter.INT:
case ClassWriter.FLOAT:
case ClassWriter.NAME_TYPE:
case ClassWriter.INDY:
size = 5;
break;
case ClassWriter.LONG:
case ClassWriter.DOUBLE:
size = 9;
++i;
break;
case ClassWriter.UTF8:
size = 3 + readUnsignedShort(index + 1);
if (size > max) {
max = size;
}
break;
case ClassWriter.HANDLE:
size = 4;
break;
// case ClassWriter.CLASS:
// case ClassWriter.STR:
// case ClassWriter.MTYPE
default:
size = 3;
break;
}
index += size;
}
// ClassWriter.FIELD等:
// 这些定义与虚拟机规范中定义的constant_pool中成员cp_info 中的tag值一致(详情参考虚拟机)
maxStringLength = max; // string最大长度
// access_flags 的索引
header = index;
}
下面再说下,springmvc什么时候进行参数解析,其实读者可以顺者 DispatcherServlet.doDispatch()方法,进行寻找,
参数的处理定是在调用Handler是完成的,这里具体的代码就不再贴出(篇幅首先,亦是没有难度的),到最后会是下面这样一个类来处理:
InvocableHandlerMethod:
// 此类的属性: 使用的是DefaultParameterNameDiscoverer这个实现类,其实就是使用的上面介绍的两种实现类,看源码就可知.
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
DefaultParameterNameDiscoverer ::
// 检测是否jdk1.8及以上版本
// 如果编译时没有使用(javac -parameters),则无法获取,会使用LocalVariableTableParameterNameDiscoverer的方式来完成方法参数名的获取
// 工作
private static final boolean standardReflectionAvailable = ClassUtils.isPresent(
"java.lang.reflect.Executable", DefaultParameterNameDiscoverer.class.getClassLoader());
// 使用了上面说的两种
public DefaultParameterNameDiscoverer() {
if (standardReflectionAvailable) {
addDiscoverer(new StandardReflectionParameterNameDiscoverer());// jdk1.8及以上版本使用
}
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
解析方法参数名时的使用是在:InvocableHandlerMethod.getMethodArgumentValues方法
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); // 通过这里设置
args[i] = resolveProvidedArgument(parameter, providedArgs); // 开始解析
if (args[i] != null) {
continue;
}
THE END!
最后
以上就是野性口红为你收集整理的java获取方法参数名称的方式的全部内容,希望文章能够帮你解决java获取方法参数名称的方式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复