如何感知方法参数名
使用SpringMVC的@PathVariable注解时,如果Mapping注解中的value值里的{}与方法参数名一致则@PathVariable是不需要写value的,这是Spring的参数自动绑定。问题来了,反射注入参数的事情我也做过,但是我做不到像它这样能感知方法参数名,它是怎么实现的呢?
@PathVariable的效果
这里有一个简单的示例:
1 2 3 4 5 6 7 8
| @RequestMapping("/say") @RestController public class SayController { @GetMapping("/{name}/{message}") public Result say(@PathVariable String message, @PathVariable String name){ return new Result(name, message); } }
|
Web程序运行后,请求路径/say/tom/hello
,显示结果取值是正确的,这就说明SpringMVC它知道参数的名字。
尝试复现
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class Test { public static void main(String[] args) { Map<String, String> dataMap = new LinkedHashMap<>(); dataMap.put("name", "minmin"); dataMap.put("message", "gugu"); }
public Result say(String message, String name) { return new Result(name, message); }
public Result say1(String name, String message) { return new Result(name, message); } private static class Result{ private String name; private String message;
public Result(String name, String message) { this.name = name; this.message = message; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; } } }
|
思路简单,反射直接拿到方法,再反射拿到方法参数对象,根据这个对象获取name属性,再对号注入参数,代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static void main(String[] args) { Map<String, String> dataMap = new LinkedHashMap<>(); dataMap.put("name", "minmin"); dataMap.put("message", "gugu"); Method[] methods = Test.class.getMethods(); for (Method method : methods) { StringBuilder stringBuilder = new StringBuilder(method.getName()); Parameter[] parameters = method.getParameters(); for (Parameter parameter : parameters) { stringBuilder.append(" ").append(parameter.getName()); } System.out.println(stringBuilder); } }
|
结果输出:
1 2 3 4 5 6 7 8 9 10 11 12
| main arg0 say arg0 arg1 say1 arg0 arg1 wait arg0 wait arg0 arg1 wait equals arg0 toString hashCode getClass notify notifyAll
|
参数名居然是arg0、arg1这样的格式,无法得知原本的参数名。
分析
Javac编译器
看到上面的输出,可以先猜是Java编译器的问题,它应该把原本的参数标识符优化掉了。
我们手动使用javac编译一下Test.java,再用IDEA打开这个class查看编译后的代码样子:
1 2 3 4 5 6 7
| public Test.Result say(String var1, String var2) { return new Test.Result(var2, var1); }
public Test.Result say1(String var1, String var2) { return new Test.Result(var1, var2); }
|
可以看到确实是编译器干的好活,但想要编译器留下原本的参数标识符还得看编译器有留下什么参数,可以用javac --help
查看帮助:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| PS D:\> javac --help 用法: javac <options> <source files> 其中, 可能的选项包括: @<filename> 从文件读取选项和文件名 -Akey[=value] 传递给注释处理程序的选项 --add-modules <模块>(,<模块>)* 除了初始模块之外要解析的根模块; 如果 <module> 为 ALL-MODULE-PATH, 则为模块路径中的所有模块。 --boot-class-path <path>, -bootclasspath <path> 覆盖引导类文件的位置 --class-path <path>, -classpath <path>, -cp <path> 指定查找用户类文件和注释处理程序的位置 -d <directory> 指定放置生成的类文件的位置 -deprecation 输出使用已过时的 API 的源位置 --enable-preview 启用预览语言功能。要与 -source 或 --release 一起使用。 -encoding <encoding> 指定源文件使用的字符编码 -endorseddirs <dirs> 覆盖签名的标准路径的位置 -extdirs <dirs> 覆盖所安装扩展的位置 -g 生成所有调试信息 -g:{lines,vars,source} 只生成某些调试信息 -g:none 不生成任何调试信息 -h <directory> 指定放置生成的本机标头文件的位置 --help, -help, -? 输出此帮助消息 --help-extra, -X 输出额外选项的帮助 -implicit:{none,class} 指定是否为隐式引用文件生成类文件 -J<flag> 直接将 <标记> 传递给运行时系统 --limit-modules <模块>(,<模块>)* 限制可观察模块的领域 --module <module-name>, -m <module-name> 只编译指定的模块, 请检查时间戳 --module-path <path>, -p <path> 指定查找应用程序模块的位置 --module-source-path <module-source-path> 指定查找多个模块的输入源文件的位置 --module-version <版本> 指定正在编译的模块版本 -nowarn 不生成任何警告 -parameters 生成元数据以用于方法参数的反射 -proc:{none,only} 控制是否执行注释处理和/或编译。 -processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程 --processor-module-path <path> 指定查找注释处理程序的模块路径 --processor-path <path>, -processorpath <path> 指定查找注释处理程序的位置 -profile <profile> 请确保使用的 API 在指定的配置文件中可用 --release <release> 针对特定 VM 版本进行编译。支持的目标: 6, 7, 8, 9, 10, 11 -s <directory> 指定放置生成的源文件的位置 -source <release> 提供与指定发行版的源兼容性 --source-path <path>, -sourcepath <path> 指定查找输入源文件的位置 --system <jdk>|none 覆盖系统模块位置 -target <release> 生成特定 VM 版本的类文件 --upgrade-module-path <path> 覆盖可升级模块位置 -verbose 输出有关编译器正在执行的操作的消息 --version, -version 版本信息 -Werror 出现警告时终止编译
|
可以看到两个关键信息:
1 2
| -g 生成所有调试信息 -parameters 生成元数据以用于方法参数的反射
|
-parameters 参数
使用-parameters
进行编译,可以在IDEA中看到编译后的效果:
1 2 3 4 5 6 7
| public Test.Result say(String message, String name) { return new Test.Result(name, message); }
public Test.Result say1(String name, String message) { return new Test.Result(name, message); }
|
这里已经可以看到参数名原本的样子了,使用java运行一下编译后的class结果:
1 2 3 4 5 6 7 8 9 10 11 12 13
| PS D:\> java com.gugu.Test main args say1 name message say message name wait arg0 wait arg0 arg1 wait equals arg0 toString hashCode getClass notify notifyAll
|
-g 参数
使用-g
进行编译,也可以同样在IDEA中查看到几乎相同的结果,再尝试使用java运行一下class输出:
1 2 3 4 5 6 7 8 9 10 11 12 13
| PS D:\> java com.gugu.Test main arg0 say arg0 arg1 say1 arg0 arg1 wait arg0 wait arg0 arg1 wait equals arg0 toString hashCode getClass notify notifyAll
|
看到这个运行结果发现居然无法通过反射获取方法参数名,问题出在哪?先来对比一下这两个参数编译后的class内容区别。
与 -parameters 的区别
使用javap命令配上v参数查看生成的class的附加信息:
-parameters
参数
重要部分如下:
1 2 3 4
| MethodParameters: Name Flags name message
|
-g
参数
重要部分如下:
1 2 3 4 5
| LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/gugu/Test; 0 10 1 name Ljava/lang/String; 0 10 2 message Ljava/lang/String;
|
接下来的问题就是如何去获取字节码中的LocalVariableTable,这个就到我的知识盲区了,借由百度得知可以使用ASM、Javassist这些框架去操作解析字节码。
ASM
spring-core中自带了ASM的封装,可以使用org.springframework.core.LocalVariableTableParameterNameDiscoverer
来解析LocalVariableTable获取方法参数名,示例代码:
1 2 3 4 5 6 7 8
| private static void asm() { LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); Method[] methods = Test.class.getMethods(); for (Method method : methods) { String[] parameterNames = discoverer.getParameterNames(method); System.out.println("method: " + method.getName() + " parameter: " + Arrays.toString(parameterNames)); } }
|
输出结果:
1 2
| method: say1 parameter: [name, message] method: say parameter: [message, name]
|
平时可以使用org.springframework.core.DefaultParameterNameDiscoverer
类,它集合了好几种方式来解析方法参数名。
Maven
对SpringBoot应用进行打包后使用javap分析,可以看到会有LocalVariableTable,Maven package 使用到了 -g
参数编译的,但具体细节暂时不谈。
结论
还得是Spring,太强大了。
另外在搜索资料的过程中知道一个有趣的事,Mybatis的Mapper接口在多参数的时候为什么需要使用@Param注解来为参数设Key,因为接口需要JDK8及以上使用-parameters
参数才可以获取参数名称,所以Mybatis才使用注解来辅助建立与参数的映射关系。