如何感知方法参数名

如何感知方法参数名

使用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");
// 如何使用反射能正确的将dataMap的数据映射到say、say1方法参数中?
}

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才使用注解来辅助建立与参数的映射关系。


如何感知方法参数名
https://blog.gugu.dev/2024-02-21/如何感知方法参数名/
作者
MinMin
发布于
2024年2月21日
许可协议