当前位置: 首页 > news >正文

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)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,这是因为通过解析参数名称后,就可以通过名称进行匹配

相关文章:

  • 左手医生:医疗 AI 企业的云原生提效降本之路
  • 线程安全(二)--死锁
  • C#_事件_多线程(基础)
  • CCF考级 1-8级考纲知识点
  • 面试吹牛宝典
  • Linux内核err.h文件分析
  • springboot基本使用八(mbatisplus+filter实现登录功能)
  • ADC重要的信噪比公式是怎么来的?
  • Python自动连接SSH
  • Redis入门三(主从复制、Redis哨兵、Redis集群、缓存更新策略、缓存穿透、缓存击穿、缓存雪崩)
  • 算法学习——LeetCode力扣动态规划篇8
  • MATLAB 自定义生成直线点云(详细介绍) (47)
  • JSQLParserException异常
  • OpenHarmony无人机MAVSDK开源库适配方案分享
  • Mac 装 虚拟机 vmware、centos7等
  • 08.Android之View事件问题
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • create-react-app项目添加less配置
  • hadoop入门学习教程--DKHadoop完整安装步骤
  • HTML-表单
  • httpie使用详解
  • Promise面试题,控制异步流程
  • 爱情 北京女病人
  • 仿天猫超市收藏抛物线动画工具库
  • 构造函数(constructor)与原型链(prototype)关系
  • 海量大数据大屏分析展示一步到位:DataWorks数据服务+MaxCompute Lightning对接DataV最佳实践...
  • 跳前端坑前,先看看这个!!
  • 项目管理碎碎念系列之一:干系人管理
  • 在weex里面使用chart图表
  • 曾刷新两项世界纪录,腾讯优图人脸检测算法 DSFD 正式开源 ...
  • # 日期待t_最值得等的SUV奥迪Q9:空间比MPV还大,或搭4.0T,香
  • #laravel 通过手动安装依赖PHPExcel#
  • $var=htmlencode(“‘);alert(‘2“); 的个人理解
  • (3)STL算法之搜索
  • (NSDate) 时间 (time )比较
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (附源码)spring boot基于Java的电影院售票与管理系统毕业设计 011449
  • .bat文件调用java类的main方法
  • .NET Core 2.1路线图
  • .net core控制台应用程序初识
  • .NET Core中Emit的使用
  • .NET高级面试指南专题十一【 设计模式介绍,为什么要用设计模式】
  • .net专家(张羿专栏)
  • /proc/stat文件详解(翻译)
  • @ 代码随想录算法训练营第8周(C语言)|Day57(动态规划)
  • [ 常用工具篇 ] AntSword 蚁剑安装及使用详解
  • [CF703D]Mishka and Interesting sum/[BZOJ5476]位运算
  • [hdu2196]Computer树的直径
  • [hive小技巧]同一份数据多种处理
  • [IT生活推荐]大家一起来玩游戏喽,来的都进!
  • [java] 23种设计模式之责任链模式
  • [Lucene] Lucene 全文检索引擎简介
  • [oeasy]python0004_游乐场_和python一起玩耍_python解释器_数学运算
  • [office] excel中weekday函数的使用方法 #学习方法#微信#媒体
  • [one_demo_14]一个简单的easyui的demo