Java获取函数参数名称

原理

编译之后的class文件默认是不带有参数名称信息的,使用 IDE 时,反编译jar包得到的源代码函数参数名称是 arg0,arg1......这种形式,这是因为编译 jar 包的时候没有把符号表编译进去。

JDK1.7 及以下版本的 API 并不能获取到函数的参数名称,需要使用字节码处理框架,如 ASM、javassist 等来实现,且需要编译器开启输出调试符号信息的参数的-g。这个过程简单描述就是:

  • 编译器javac使用-g输出调试符号信息到class文件
  • 程序通过字节码解析框架解析class文件获取函数参数名称

显然,通过字节码框架的方式需要读文件,不够优雅。jdk8提供了反射机制直接获取函数参数名称,即在javac命令上加上 -parameter参数,这个参数默认是关闭的。

著名的 ORM 框架 Mybatis 就使用了函数参数注解的方式来实现,使用函数参数注解的方法具有如下优点:

  • 框架实现简单
  • 可以灵活设置参数,在注解中绑定的是字符串即可,无需是合法的标识符。

使用 ASM

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

public class TestMain {

public static void main(String[] args) {

	Class<?> clazz = TestMain.class;
	try {
		Method method = clazz.getDeclaredMethod("test", String.class,
				int.class);
		String[] pns = getParameterNamesByAsm5(clazz, method);
		System.out.print(method.getName() + " : ");
		for (String parameterName : pns) {
			System.out.print(parameterName + ' ');
		}
	} catch (NoSuchMethodException e) {
		e.printStackTrace();
	} catch (SecurityException e) {
		e.printStackTrace();
	}
}

public static void test(String param1, int param2) {
	System.out.println(param1 + param2);
}

public static String[] getParameterNamesByAsm5(Class<?> clazz,
		final Method method) {
	final Class<?>[] parameterTypes = method.getParameterTypes();
	if (parameterTypes == null || parameterTypes.length == 0) {
		return null;
	}
	final Type[] types = new Type[parameterTypes.length];
	for (int i = 0; i < parameterTypes.length; i++) {
		types[i] = Type.getType(parameterTypes[i]);
	}
	final String[] parameterNames = new String[parameterTypes.length];

	String className = clazz.getName();
	int lastDotIndex = className.lastIndexOf(".");
	className = className.substring(lastDotIndex + 1) + ".class";
	InputStream is = clazz.getResourceAsStream(className);
	try {
		ClassReader classReader = new ClassReader(is);
		classReader.accept(new ClassVisitor(Opcodes.ASM5) {
			@Override
			public MethodVisitor visitMethod(int access, String name,
					String desc, String signature, String[] exceptions) {
				// 只处理指定的方法
				Type[] argumentTypes = Type.getArgumentTypes(desc);
				if (!method.getName().equals(name)
						|| !Arrays.equals(argumentTypes, types)) {
					return super.visitMethod(access, name, desc, signature,
							exceptions);
				}
				return new MethodVisitor(Opcodes.ASM5) {
					@Override
					public void visitLocalVariable(String name, String desc,
							String signature, org.objectweb.asm.Label start,
							org.objectweb.asm.Label end, int index) {
						// 非静态成员方法的第一个参数是this
						if (Modifier.isStatic(method.getModifiers())) {
							parameterNames[index] = name;
						} else if (index > 0) {
							parameterNames[index - 1] = name;
						}
					}
				};
			}
		}, 0);
	} catch (IOException e) {
	} finally {
		try {
			if (is != null) {
				is.close();
			}
		} catch (Exception e2) {
		}
	}
	return parameterNames;
}

}

使用 javaassist

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;

public class TestMain {

	public static void main(String[] args) {
		Class<?> clazz = TestMain.class;
		ClassPool pool = ClassPool.getDefault();
		try {
			CtClass ctClass = pool.get(clazz.getName());
			CtMethod ctMethod = ctClass.getDeclaredMethod("test");

			// 使用javassist的反射方法的参数名
			MethodInfo methodInfo = ctMethod.getMethodInfo();
			CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
			LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute
					.getAttribute(LocalVariableAttribute.tag);
			if (attr != null) {
				int len = ctMethod.getParameterTypes().length;
				// 非静态的成员函数的第一个参数是this
				int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
				System.out.print("test : ");
				for (int i = 0; i < len; i++) {
					System.out.print(attr.variableName(i + pos) + ' ');
				}
				System.out.println();
			}
		} catch (NotFoundException e) {
			e.printStackTrace();
		}
	}

	public static void test(String param1, int param2) {
		System.out.println(param1 + param2);
	}
}

Spring 对 ASM 的封装

spring-core 中的 LocalVariableTableParameterNameDiscoverer 它对 ASM 进行了封装。

import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;

public class TestMain {

	public static void main(String[] args) {
		ParameterNameDiscoverer parameterNameDiscoverer =
				new LocalVariableTableParameterNameDiscoverer();
		try {
			String[] parameterNames = parameterNameDiscoverer
					.getParameterNames(TestMain.class.getDeclaredMethod("test",
							String.class, int.class));
			System.out.print("test : ");
			for (String parameterName : parameterNames) {
				System.out.print(parameterName + ' ');
			}
		} catch (NoSuchMethodException | SecurityException e) {
			e.printStackTrace();
		}
	}

	public static void test(String param1, int param2) {
		System.out.println(param1 + param2);
	}
}

jdk8中自带函数参数功能

在Java1.8之后,可以通过反射API java.lang.reflect.Executable.getParameters来获取到方法参数的元信息,这要求在使用编译器时加上-parameters参数(javac默认不带此参数),它会在生成的.class文件中额外存储参数的元信息,这会增加class文件的大小。

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
 
public class TestMain {
 
	public static void main(final String[] arguments) throws Exception {
		Class<?> clazz = TestMain.class;
		Method method = clazz.getDeclaredMethod("test", String.class, int.class);
		System.out.print("test : ");
		Parameter[] parameters = method.getParameters();
		for (final Parameter parameter : parameters) {
			if (parameter.isNamePresent()) {
				System.out.print(parameter.getName() + ' ');
			}
		}
	}
 
	public void test(String param1, int param2) {
		System.out.println(param1 + param2);
	}
}

使用注解

上面介绍的几种方法都需要依赖编译器附加一定的编译参数,才能获取到。如果程序编译不想保留这些调试信息和附加的元数据,或者你开发一个了框架提供给别人使用,可是该框架想要获取用户代码的函数参数名,因为你并不能控制别人怎么编译Java代码,这时怎么提供一种不受编译器影响的途径来确保获取到函数参数名呐?看看spring mvc是怎么做的——使用函数参数注解。

定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Parameter {
	String value();
}

使用注解的例子

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
 
public class TestMain {
 
	public static void main(String[] args) throws Exception {
		Method method = TestMain.class.getMethod("test", String.class, int.class);
		System.out.print("test : ");
		Annotation parameterAnnotations[][] = method.getParameterAnnotations();
		for (int i = 0; i < parameterAnnotations.length; i++) {
			for (Annotation annotation : parameterAnnotations[i]) {
				if (Parameter.class.equals(annotation.annotationType())) {
					System.out.print(((Parameter) annotation).value() + ' ');
				}
			}
		}
	}
 
	public void test(@Parameter("param1") String param1,
			@Parameter("param2") int param2) {
		System.out.println(param1 + param2);
	}
}

参考资料 一篇文章详细解读 SpringMVC 的函数参数绑定过程

Java获取函数参数名称的几种方法:https://blog.csdn.net/wwwwenl/article/details/53427039
https://www.cnblogs.com/guangshan/p/4660564.html

廖雪峰:Java8 中获取函数参数名称

原文地址:https://www.cnblogs.com/weiyinfu/p/8339825.html