java 获取方法参数名的若干实践

本贴最后更新于 2778 天前,其中的信息可能已经时移俗易

前言

我们知道 java 可以通过反射得到方法名、参数类型等信息。但我们似乎不能直接得到方法的参数名。而在一些场景中,比如构建自己的 MVC 框架时,我们也想像 Spring MVC 一样,根据参数名获取用户传来的数据。下面就来总结一下,都有哪些方法可以获得方法的参数名。

1. 使用 java8

自 java8 开始,可以直接通过反射得到方法的参数名。取代了之前如 arg0、arg1 等无含义的参数名称。不过这样有个条件:你必须手动在编译时开启 -parameters 参数,否则还是获取不到。

以 IDEA 为例,你需要在 Preferences->Build,Execution,Deployment->Compiler->java Compiler 页面添加该编译选项

这里写图片描述

下面就是代码了

public class TestMethodArg {

    public void  method1(String name,String email){
        System.out.println(name+":"+email);

    }
    @Test
    public void test(){
        Class<TestMethodArg> clazz = TestMethodArg.class;
        try {
            //得到方法实体
            Method method = clazz.getMethod("method1", String.class, String.class);
            //得到该方法参数信息数组
            Parameter[] parameters = method.getParameters();
            //遍历参数数组,依次输出参数名和参数类型
            Arrays.stream(parameters).forEach(p->{
                System.out.println(p.getName()+" : "+p.getType());
            });
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

name : class java.lang.String
email : class java.lang.String

可见该方法很简单,重点在于 Parameter 这个类,其保存了所代表的这个参数信息,包括这个参数类型、名称、注解等。可惜这个类是 java8 新加的,只能在 jdk8 及以后版本使用。

可以试试去掉 -parameters 参数,结果会看到参数名又变成 arg0、arg1 等名称了。

2. 使用 javassist 获取参数名

使用 java8 方法有局限性,没办法,既然原生的只能帮我们到这,那我们就尝试使用第三方类库了。比较有名的 java 字节码操作类库如 javassistasmcglib 都可以办到。据说 cglib 底层使用 asm 实现。我们重点研究前两个。

先看看 javassistjavassist 是一个处理 java 字节码的类库。现已加入 JBoss 应用服务器项目。JBoss 就是使用它作为实现 AOP 的框架。

可以看看这篇文章 Javassist 使用指南(一) 来了解。简单来说,CtClass 表示类对象;CtMethod 表示方法;ClassPool 表示 CtClass 对象的容器,可从这里获取 CtClass。其他的我们通过代码来实践。

  @Test
    public void test3() {
        try {
            //获取要操作的类对象
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.get("com.wthfeng.learn.classtest.Sample");

            //获取要操作的方法参数类型数组,为获取该方法代表的CtMethod做准备
            Method method = Sample.class.getMethod("start", String.class);
            int count = method.getParameterCount();
            Class<?>[] paramTypes = method.getParameterTypes();
            CtClass[] ctParams = new CtClass[count];
            for (int i = 0; i < count; i++) {
                ctParams[i] = pool.getCtClass(paramTypes[i].getName());
            }
            
            CtMethod ctMethod = ctClass.getDeclaredMethod("start", ctParams);
            //得到该方法信息类
            MethodInfo methodInfo = ctMethod.getMethodInfo();

            //获取属性变量相关
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();

            //获取方法本地变量信息,包括方法声明和方法体内的变量
            //需注意,若方法为非静态方法,则第一个变量名为this
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            int pos = Modifier.isStatic(method.getModifiers()) ? 0 : 1;

            for (int i = 0; i < count; i++) {
                System.out.println(attr.variableName(i + pos));

            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
    }

Sample 是一个简单的测试类

class Sample {
    private String name;

    public void start(String tag) {
        int i = 0;
        String abc = "abc";
        System.out.println(i);
        System.out.println(tag);
        System.out.println(abc);
    }
}

结果输出 tag
控制循环的大小,还可以输出 i 、abc 等方法体内的变量,这里就不演示了。

3. 使用 ASM 获取方法参数名

关于 ASM 的介绍,可以参考 AOP 的利器:ASM 3.0 介绍这篇文章。

ASM 也是一个 java 字节码操作类库,不同的是它采用事件驱动模型。java class 被描述为一棵树,使用 visitor 模式遍历类结构,并在需要时进行修改。

其中主要的类有 ClassReader,它可以通过字节数组或 class 文件获取字节码数据用于后面对字节码的操作。可以认为是字节码的生产者。其主要方法是 accept,接受以 ClassVisitor、ClassAdapter 所代表的消费者对字节码的操作。操作均以遍历的形式进行。下面我们来看看

 @Test
    public void test() {
        try {

            // 读取HelloTest的字节码信息到ClassReader中
            ClassReader reader = new ClassReader(HelloTest.class.getName());
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            //accept接收了一个ClassAdapter的子类,想要操作什么,就在子类实现什么
            reader.accept(new ClassAdapter(cw) {

                /**
                 * 会遍历该类的所有方法,你可以对不需要操作的方法直接返回
                 */
                @Override
                public MethodVisitor visitMethod(final int access, final String name, final String desc,
                                                 final String signature, final String[] exceptions) {
                    //不需要操作的方法,直接返回,注意不要返回null,会把该方法删掉
                    if (!name.equals("test1")) {
                        return super.visitMethod(access, name, desc, signature, exceptions);
                    }
                    MethodVisitor v = super.visitMethod(access, name, desc,
                            signature, exceptions);
                    /**
                     *  遍历该方法信息,比如参数、注解等,这里我们要操作参数,所以实现了参数方法
                     */
                    return new MethodAdapter(v) {
                        public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
                            //如果是静态方法,第一个参数就是方法参数,非静态方法,则第一个参数是 this ,然后才是方法的参数
                            System.out.println(name + "," + index);

                            super.visitLocalVariable(name, desc, signature, start, end, index);
                        }
                    };
                }
            }, 0);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

HelloTest 是测试类,代码如下:

class HelloTest {
    public void test1(String userName) {
        System.out.println(userName);
    }

    public void test2(String email) {
        System.out.println(email);
    }
}

我们这里获取的是 test1 这个方法的参数名,输出如下:

this,0
userName,1

可见如愿获得了参数名,如果你不想获取 this,可参考 javassist 的例子。

4. Spring MVC 是怎样获取的

一开始我们就提过,spring mvc 框架本身支持这个功能,那何不看看它是怎么实现的?

Spring MVC 的 DefaultParameterNameDiscoverer 负责实现这个功能。大体思路是:

  1. 判断是否是 java8(通过 Executable 这个 java8 引入的类判断,Parameter 许多方法使用了该类),若是,尝试使用 java8 的方法获取。
  2. 使用经 spring 封装的 ASM 获取,本质还是使用 ASM。
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {

	private static final boolean standardReflectionAvailable = ClassUtils.isPresent(
			"java.lang.reflect.Executable", DefaultParameterNameDiscoverer.class.getClassLoader());


	public DefaultParameterNameDiscoverer() {
	    //java8的方式
		if (standardReflectionAvailable) {
			addDiscoverer(new StandardReflectionParameterNameDiscoverer());
		}
		//ASM的方式
		addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
	}

}

具体实现代码就不贴了。

----------全文完------------

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
  • asm
    15 引用 • 5 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...