默认情况下编译时,不会带上方法参数名称,例如通过javac ./ParamNameResolverTest.java
编译如下类
public class ParamNameResolverTest {
public void test(String name, int age) {}
}
编译的结果如下:
public class ParamNameResolverTest {
public ParamNameResolverTest() {
}
public void test(String var1, int var2) {
}
}
SpringBoot在编译时会加上-parameters参数,即javac -parameters .\ParamNameResolverTest.java
,会生成参数表,通过反射能够获取到方法参数名,编译后通过javap -c -v ./ParamNameResolverTest.java
查看字节码,多了MethodParameters信息,如下:
public void test(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 6: 0
MethodParameters:
Name Flags
name
age
通过MethodParameters中的信息,就可以获取到参数名称
IDEA工具编译时会带上-g参数,即javac -g .\ParamNameResolverTest.java
,如果是类,方法参数名会写入到局部变量表中,能够通过ASM获取到参数名,如果是接口,则无法获取到参数名
编译后通过javap -c -v ./ParamNameResolverTest.java
查看字节码,多了LocalVariableTable信息,如下:
public void test(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/limin/study/springmvc/A03/ParamNameResolverTest;
0 1 1 name Ljava/lang/String;
0 1 2 age I
通过LocalVariableTable信息也能够获取到参数名称
RequestMappingHandlerAdapter会创建DefaultParameterNameDiscoverer对象,它添加了两种解析器能够分别处理上述的两种情况,见DefaultParameterNameDiscoverer源码
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
public DefaultParameterNameDiscoverer() {
if (KotlinDetector.isKotlinReflectPresent() && !GraalDetector.inImageCode()) {
this.addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
// 通过反射获取方法参数名称
this.addDiscoverer(new StandardReflectionParameterNameDiscoverer());
// 通过局部变量表获取方法参数名称
this.addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
}
1)StandardReflectionParameterNameDiscoverer通过反射获取方法参数名称
public class StandardReflectionParameterNameDiscoverer implements ParameterNameDiscoverer {
@Override
@Nullable
public String[] getParameterNames(Method method) {
return getParameterNames(method.getParameters());
}
@Override
@Nullable
public String[] getParameterNames(Constructor<?> ctor) {
return getParameterNames(ctor.getParameters());
}
@Nullable
private String[] getParameterNames(Parameter[] parameters) {
String[] parameterNames = new String[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter param = parameters[i];
// isNamePresent判断MethodParameters中的参数名是否存在,可查看JDK文档
if (!param.isNamePresent()) {
return null;
}
// 返回参数名称
parameterNames[i] = param.getName();
}
return parameterNames;
}
}
Parameter中的isNamePresent方法可以判断MethodParameters中的参数名是否存在,getName方法返回参数名称
2)LocalVariableTableParameterNameDiscoverer使用ASM通过局部变量表获取方法参数名称
public class LocalVariableTableParameterNameDiscoverer implements ParameterNameDiscoverer {
@Override
@Nullable
public String[] getParameterNames(Method method) {
// 如果是桥接方法返回原始方法,否则直接返回method
Method originalMethod = BridgeMethodResolver.findBridgedMethod(method);
return doGetParameterNames(originalMethod);
}
@Override
@Nullable
public String[] getParameterNames(Constructor<?> ctor) {
return doGetParameterNames(ctor);
}
@Nullable
private String[] doGetParameterNames(Executable executable) {
Class<?> declaringClass = executable.getDeclaringClass();
// 调用inspectClass读取字节码中的局部变量表
Map<Executable, String[]> map = this.parameterNamesCache.computeIfAbsent(declaringClass, this::inspectClass);
return (map != NO_DEBUG_INFO_MAP ? map.get(executable) : null);
}
private Map<Executable, String[]> inspectClass(Class<?> clazz) {
// 获取类的字节码文件流
InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz));
// 省略其他代码...
try {
ClassReader classReader = new ClassReader(is);
Map<Executable, String[]> map = new ConcurrentHashMap<>(32);
// 1.通过ASM读取字节码文件
classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0);
return map;
}
// 省略其他代码...
}
private static class ParameterNameDiscoveringVisitor extends ClassVisitor {
// 省略其他代码...
@Override
@Nullable
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (!isSyntheticOrBridged(access) && !STATIC_CLASS_INIT.equals(name)) {
// 返回LocalVariableTableVisitor对象
return new LocalVariableTableVisitor(this.clazz, this.executableMap, name, desc, isStatic(access));
}
return null;
}
}
private static class LocalVariableTableVisitor extends MethodVisitor {
// 省略其他代码...
@Override
public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) {
this.hasLvtInfo = true;
for (int i = 0; i < this.lvtSlotIndex.length; i++) {
if (this.lvtSlotIndex[i] == index) {
// 获取参数名称
this.parameterNames[i] = name;
}
}
}
@Override
public void visitEnd() {
if (this.hasLvtInfo || (this.isStatic && this.parameterNames.length == 0)) {
// 方法处理结束时添加到executableMap中
this.executableMap.put(resolveExecutable(), this.parameterNames);
}
}
private Executable resolveExecutable() {
ClassLoader loader = this.clazz.getClassLoader();
// 获取参数类型
Class<?>[] argTypes = new Class<?>[this.args.length];
for (int i = 0; i < this.args.length; i++) {
argTypes[i] = ClassUtils.resolveClassName(this.args[i].getClassName(), loader);
}
try {
if (CONSTRUCTOR.equals(this.name)) {
// 如果是构造器,则返回构造器方法
return this.clazz.getDeclaredConstructor(argTypes);
}
// 否则则返回普通方法
return this.clazz.getDeclaredMethod(this.name, argTypes);
}
catch (NoSuchMethodException ex) {
throw new IllegalStateException("Method [" + this.name +
"] was discovered in the .class file but cannot be resolved in the class object", ex);
}
}
// 计算局部变量表中参数的索引
private static int[] computeLvtSlotIndices(boolean isStatic, Type[] paramTypes) {
int[] lvtIndex = new int[paramTypes.length];
// 如果是静态方法则第一个slot没有this,否则第一个slot放this
int nextIndex = (isStatic ? 0 : 1);
for (int i = 0; i < paramTypes.length; i++) {
lvtIndex[i] = nextIndex;
if (isWideType(paramTypes[i])) {
nextIndex += 2;
}
else {
nextIndex++;
}
}
return lvtIndex;
}
// long和double占2个slot
private static boolean isWideType(Type aType) {
return (aType == Type.LONG_TYPE || aType == Type.DOUBLE_TYPE);
}
}
}
doGetParameterNames中会调用classReader.accept通过访问者模式读取字节码信息,在这个过程中会调用readMethod方法调用ParameterNameDiscoveringVisitor的methodVisitor方法创建LocalVariableTableVisitor对象
private int readMethod(final ClassVisitor classVisitor, final Context context, final int methodInfoOffset) {
// 省略其他代码...
// 调用classVisitor.visitMethod,此处返回的是LocalVariableTableVisitor对象
MethodVisitor methodVisitor =
classVisitor.visitMethod(
context.currentMethodAccessFlags,
context.currentMethodName,
context.currentMethodDescriptor,
signatureIndex == 0 ? null : readUtf(signatureIndex, charBuffer),
exceptions);
// 省略其他代码...
if (codeOffset != 0) {
methodVisitor.visitCode();
// 读取字节码的各个部分
readCode(methodVisitor, context, codeOffset);
}
// 调用visitEnd
methodVisitor.visitEnd();
// 省略其他代码...
}
随后调用readCode方法读取字节码的各个部分,而其中LocalVariableTableVisitor对象会访问visitLocalVariable局部变量表,从中获取其中的参数名称
private void readCode(final MethodVisitor methodVisitor, final Context context, final int codeOffset) {
// 省略其他代码...
int localVariableTableLength = readUnsignedShort(localVariableTableOffset);
currentOffset = localVariableTableOffset + 2;
while (localVariableTableLength-- > 0) {
int startPc = readUnsignedShort(currentOffset);
int length = readUnsignedShort(currentOffset + 2);
String name = readUTF8(currentOffset + 4, charBuffer);
String descriptor = readUTF8(currentOffset + 6, charBuffer);
int index = readUnsignedShort(currentOffset + 8);
currentOffset += 10;
String signature = null;
if (typeTable != null) {
for (int i = 0; i < typeTable.length; i += 3) {
if (typeTable[i] == startPc && typeTable[i + 1] == index) {
signature = readUTF8(typeTable[i + 2], charBuffer);
break;
}
}
}
// 访问局部变量表,调用LocalVariableTableVisitor中的visitLocalVariable方法
methodVisitor.visitLocalVariable(
name, descriptor, signature, labels[startPc], labels[startPc + length], index);
}
// 省略其他代码...
}
SpringMVC中通过DefaultParameterNameDiscoverer获取到方法参数名称后,可以进行日志打印、参数解析等,举个例子:Handler方法中有两个参数都被@RequestParam修饰
@Controller
public class Controller01 {
@GetMapping("/test01")
public void test01(@RequestParam String a2, @RequestParam String a1, HttpServletResponse response) throws IOException {
System.out.println("a1: " + a1);
System.out.println("a2: " + a2);
response.getWriter().print("hello");
}
}
浏览器发起http://127.0.0.1:8080/test01?a1=1&a2=2调用后,即可打印a1:1和a2:2,这是因为通过解析参数名称后,就可以通过名称进行匹配