SpringMVC源码分析(六)--参数名称解析器

默认情况下编译时,不会带上方法参数名称,例如通过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,这是因为通过解析参数名称后,就可以通过名称进行匹配

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lm_ylj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值