默认情况下编译时,不会带上方法参数名称,例如通过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)Vflags: ACC_PUBLICCode:stack=0, locals=3, args_size=30: returnLineNumberTable:line 6: 0MethodParameters:Name Flagsnameage
通过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)Vflags: ACC_PUBLICCode:stack=0, locals=3, args_size=30: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 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@Nullablepublic String[] getParameterNames(Method method) {return getParameterNames(method.getParameters());}@Override@Nullablepublic String[] getParameterNames(Constructor<?> ctor) {return getParameterNames(ctor.getParameters());}@Nullableprivate 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@Nullablepublic String[] getParameterNames(Method method) {// 如果是桥接方法返回原始方法,否则直接返回methodMethod originalMethod = BridgeMethodResolver.findBridgedMethod(method);return doGetParameterNames(originalMethod);}@Override@Nullablepublic String[] getParameterNames(Constructor<?> ctor) {return doGetParameterNames(ctor);}@Nullableprivate 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@Nullablepublic 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 {// 省略其他代码...@Overridepublic 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;}}}@Overridepublic 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放thisint 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个slotprivate 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);}// 调用visitEndmethodVisitor.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,这是因为通过解析参数名称后,就可以通过名称进行匹配