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中处理方法参数名的部分代码:
处理方法参数名的接口是: ParameterNameDiscoverer
这里看两个实现:
StandardReflectionParameterNameDiscoverer(jdk8以上版本使用,这里不贴代码,因为比较简单)
LocalVariableTableParameterNameDiscoverer(jdk8以前版本使用)

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) {
        map = inspectClass(declaringClass); // 获取字节码,解析class文件中常量池,下方为其代码
        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);
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; // 常量池开始位置

        // 以下代码,水平有限,不能详细描述,个人觉得就是寻找LocalVariableTable
        // 因为方法的参数名信息只能通过 LocalVariableTable 才可知道方法名在常量池中索引位置,才能找到名为CONSTANT_Utf8_info结构
        // 的数据,最终对应的就是一个CONSTANT_Utf8_info结构的东西

        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值一致(详情参考虚拟机)


        // something code...
    }
------------------------------------------------------------------------------------------------------------------------


spring mvc 中设置ParameterNameDiscoverer 地方挺多的,通过DispatcherServlet中的doDispatch()这条链有一处:

InvocableHandlerMethod:

private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();


DefaultParameterNameDiscoverer中对于使用了上面介绍的两种实现类:
DefaultParameterNameDiscoverer代码:
------------------------------------------------------------------------------------------------------------------------
// 检测是否jdk1.8及以上版本
private static final boolean standardReflectionAvailable = ClassUtils.isPresent(
            "java.lang.reflect.Executable", DefaultParameterNameDiscoverer.class.getClassLoader());


    public DefaultParameterNameDiscoverer() {
        if (standardReflectionAvailable) {
            addDiscoverer(new StandardReflectionParameterNameDiscoverer());
        }
        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;
            }
------------------------------------------------------------------------------------------------------------------------

读者可以一边看一边通过 javap, javac 等 命令对上面介绍的第2、3两种方式进行测试查看 LocalVariableTable,

以上说的class文件结构,均是指但不限于 java源码文件编译后生成  .class文件
THE END!

猜你喜欢

转载自blog.csdn.net/AWEI1024/article/details/81145964
今日推荐