原理
編譯之后的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