前言
你是否曾经好奇过 SpringMVC 是如何获取方法参数名实现请求参数映射的呢?是反射还是字节码技术?最近群友在知乎回答了该问题,苦于没有博客,特此转载分享 Java 中获取方法参数名的原理。
ps. 该群友单身优质男青年,95后,在线找女票ing,有意者mm
原文出处:https://zhuanlan.zhihu.com/p/610288146
作者:xinxi
javac 命令
Java里面获取方法的参数名大概有两种方法,对应的javac的两个选项如下
-g 选项
生成调试用的东西,它有三个,lines、vars、source,也就是调试的时候用的行号、参数名和源文件。直接使用 -g 的话会把这三个信息都生成。
编译时使用 -g 选项,然后使用 javap 可以看到会有一个 LocalVariableTable 块,里面有方法的参数的名字,如下图所示
-parameters 选项
直接看效果吧,它有个 MethodParameters 块,如下图
使用代码获取 LocalVariableTable 块
我们自己去读取 class 文件貌似有点难度,借助一些处理字节码的框架会比较ok
使用ASM
import org.springframework.asm.*;
import static org.springframework.asm.Opcodes.*;
public class Main {
public static void main(String[] args) throws Exception {
ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("com.example.test.Dog");
cr.accept(cp, 0);
}
}
class ClassPrinter extends ClassVisitor {
public ClassPrinter() {
super(ASM9);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("---------------------------------------");
System.out.println(name + " | " + desc);
return new MethodVisitor(ASM9) {
@Override
public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {
System.out.println(name + " t " + descriptor);
super.visitLocalVariable(name, descriptor, signature, start, end, index);
}
};
}
}
这里用的是 Spring ASM,与原生的差不太多,运行效果如下
使用javassist
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;
import java.lang.reflect.Modifier;
public class Main {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("com.example.test.Dog");
CtMethod ctMethod = ctClass.getDeclaredMethod("func");
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
if (attr != null) {
int len = ctMethod.getParameterTypes().length;
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = 0; i < len; i++) {
System.out.print(attr.variableName(i + pos) + ' ');
}
System.out.println();
}
}
}
运行效果如下
使用代码获取 MethodParameters 块
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class Main {
public static void main(String[] args) throws Exception {
Class<?> clazz = Dog.class;
Method method = clazz.getDeclaredMethod("func", String.class, Integer.class);
Parameter[] parameters = method.getParameters();
for (final Parameter parameter : parameters) {
if (parameter.isNamePresent()) {
System.out.print(parameter.getName() + ' ');
}
}
}
}
运行效果如下
构建工具
写Java的应该很少有手动 javac 的吧?所以看看构建工具是很有必要的。
Maven
Maven编译代码使用的是 maven-compiler-plugin 插件,看看它是怎么玩的
amaven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html
可以看到 debug 默认 true,parameters 默认 false。
Gradle
参考
adocs.gradle.org/current/dsl/org.gradle.api.tasks.compile.CompileOptions.html
可以看到 debug 默认是 true。
没找到 parameters…..你可以自己指定这个选项,默认应该是没有开启这个。
SpringBoot 项目
-
Maven
如果你是下面这样写的话
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
那么可以看到编译插件被动了点手脚,如下
还记得么,Maven的编译插件 debug 默认 true,parameters 默认 false,而SpringBoot把parameters也打开了。
-
Gradle
我们直接查看SpringBoot的Gradle插件源码如下
还记得么, Gradle编译时debug 默认是 true,parameters 默认 false。而SpringBoot插件会检查如果没有 -parameters 的话,就加上去。
Spring 框架
spring-core 模块中有个 ParameterNameDiscoverer 接口,专门用来获取参数的名字。比较重要的实现是如下两个
-
StandardReflectionParameterNameDiscoverer 类
使用JDK 8的反射设施来反省参数名称(编译时需指定 -parameters 参数)
-
LocalVariableTableParameterNameDiscoverer 类
使用 ASM 库来分析类文件,使用方法属性中的 LocalVariableTable 信息来发现参数名称(编译时需指定 -g 参数生成调试信息)
但是实际上使用的类是 DefaultParameterNameDiscoverer,源代码如下
一看就应该知道是怎么工作的,把能用的手段都用上对吧。
测试代码如下
import com.google.common.collect.Lists;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
public class Main {
public static void main(String[] args) {
Lists.newArrayList(
new LocalVariableTableParameterNameDiscoverer(),
new StandardReflectionParameterNameDiscoverer(),
new DefaultParameterNameDiscoverer()
).forEach(parameterNameDiscoverer -> {
try {
String[] parameterNames = parameterNameDiscoverer
.getParameterNames(Dog.class.getDeclaredMethod("func", String.class, Integer.class));
for (String parameterName : parameterNames) {
System.out.print(parameterName + ' ');
}
System.out.println();
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
}
}
运行效果如下
既没有-g也没有-parameters还能抢救一下吗?
能的。用注解,不过这已经不算是获取方法的参数名了,但也能用不是。。。
原文始发于微信公众号(Kirito的技术分享):获取Java方法参数名的原理与实践