java获取方法参数名称的方式

经过私下里的思考,对文章进行了更改


有时候我们需要通过获取方法的参数名称来完成一些业务需求,比如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!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值