Poison

Java 运行时获取方法参数名称

最近开发 BPM 流程编排引擎,需要将部分 Java 方法注册为业务节点供界面上管理业务流程,此时需要获取 Java 方法的签名,其中参数名获取出来是 arg0, arg1, arg2 这样的,查询了下文档,.class 文件默认没有包含形式参数名称,因为含有形式参数名称时需要更大的静态及动态空间占用且部分场景形式参数名称会暴露安全敏感方法的信息,需要编译时加上 -parameters 以保留形式参数名称至运行时。

其中 JDK 中获取参数的源码位于 Parameter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Returns the name of the parameter. If the parameter's name is
* {@linkplain #isNamePresent() present}, then this method returns
* the name provided by the class file. Otherwise, this method
* synthesizes a name of the form argN, where N is the index of
* the parameter in the descriptor of the method which declares
* the parameter.
*
* @return The name of the parameter, either provided by the class
* file or synthesized if the class file does not provide
* a name.
*/
public String getName() {
// Note: empty strings as paramete names are now outlawed.
// The .equals("") is for compatibility with current JVM
// behavior. It may be removed at some point.
if(name == null || name.equals(""))
return "arg" + index;
else
return name;
}

从以上源码可以看出 arg0, arg1, arg2 的来源,同理在 MyBatis 框架中,引入了 @Param 注解来处理默认运行时无法获取真实参数名的问题,其中读取 @Param 注解值的源码位于 ParamNameResolver

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
public ParamNameResolver(Configuration config, Method method) {
this.useActualParamName = config.isUseActualParamName();
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (useActualParamName) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}

可以看出,获取方法参数名时,优先使用 @Param 注解配置的名称,如果获取不到再尝试使用 JDK 的方法获取,再获取不到就直接用序号表示。但是在 Spring MVC 中是如何获取到 Controller 层方法中的参数名并进行映射的呢?我查询了相关实现,发现 Spring MVC 是使用 ASM 获取的 class 文件中方法的 debug 信息中的参数名称,源码位于:LocalVariableTableParameterNameDiscoverer.java at v5.3.17,其中注释提到:

Implementation of ParameterNameDiscoverer that uses the LocalVariableTable information in the method attributes to discover parameter names. Returns null if the class file was compiled without debug information.

即使用方法属性中的 LocalVariableTable 信息获取参数名称,如果 class 文件编译时不含 debug 信息则返回空。查询 IDEA 的默认配置我们知道,默认编译时是带有 debug 信息的,默认配置如下:

类似地,我们发现 Maven 的编译插件配置中,debug 信息也是默认开启的,可以参考:Apache Maven Compiler Plugin – compiler:compile。那么为什么 MyBatis 不采用类似的方法解析 Mapper 接口中方法的参数名称呢?经过查询,Java Virtual Machine Specification 中给出了解释:

The LocalVariableTable attribute is an optional variable-length attribute in the attributes table of a Code attribute (§4.7.3). It may be used by debuggers to determine the value of a given local variable during the execution of a method.

LocalVariableTable 属性是位于 Code 属性中的,我们继续看 Code 属性的解释:

The Code attribute is a variable-length attribute in the attributes table of a method_info structure (§4.6). A Code attribute contains the Java Virtual Machine instructions and auxiliary information for a method, including an instance initialization method and a class or interface initialization method (§2.9.1, §2.9.2).

If the method is either native or abstract, and is not a class or interface initialization method, then its method_info structure must not have a Code attribute in its attributes table. Otherwise, its method_info structure must have exactly one Code attribute in its attributes table.

即如果一个方法是 nativeabstract 的,且非初始化方法,那么它的 method_info 结构中就不含 Code 属性,我们可以用 javac -g 来验证,-g 即生成 debug 信息至 class 文件中,完整的 javac 命令可参考 javac,我们先看一个简单的接口定义:

1
2
3
4
5
public interface TestInterface {

void method(String parameterName);

}

使用 javac -g TestInterface.java 编译该接口再使用 javap -v TestInterface 反汇编 class 文件,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Classfile /private/tmp/TestInterface.class
Last modified Mar 17, 2022; size 148 bytes
MD5 checksum da2f58afc0eaf77badc94c90de385198
Compiled from "TestInterface.java"
public interface TestInterface
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #7 // TestInterface
#2 = Class #8 // java/lang/Object
#3 = Utf8 method
#4 = Utf8 (Ljava/lang/String;)V
#5 = Utf8 SourceFile
#6 = Utf8 TestInterface.java
#7 = Utf8 TestInterface
#8 = Utf8 java/lang/Object
{
public abstract void method(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "TestInterface.java"

与规范中描述的一致,方法属性中并不存在 Code 属性,自然也就没有 LocalVariableTable 属性,所以 MyBatis 无法通过 Spring MVC 中采用的方法获取到接口方法中的参数名称,而 Spring MVC 可以获取到是因为我们编写的 Controller 中的方法均为非抽象方法。我们再看一个普通的类:

1
2
3
4
5
6
7
public class TestClass {

public void method(String parameterName) {

}

}

使用 javac -g TestClass.java 编译该类再使用 javap -v TestClass 反汇编 class 文件,输出如下:

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
Classfile /private/tmp/TestClass.class
Last modified Mar 17, 2022; size 389 bytes
MD5 checksum 8e124ecce6632ad6e1a5bb45888a3168
Compiled from "TestClass.java"
public class TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#17 // java/lang/Object."<init>":()V
#2 = Class #18 // TestClass
#3 = Class #19 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 LTestClass;
#11 = Utf8 method
#12 = Utf8 (Ljava/lang/String;)V
#13 = Utf8 parameterName
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 SourceFile
#16 = Utf8 TestClass.java
#17 = NameAndType #4:#5 // "<init>":()V
#18 = Utf8 TestClass
#19 = Utf8 java/lang/Object
{
public TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTestClass;

public void method(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LTestClass;
0 1 1 parameterName Ljava/lang/String;
}
SourceFile: "TestClass.java"

可见,class 文件的方法属性中是含有 Code 属性的,其中的 LocalVariableTable 属性含有每个变量的参数名称,同时我们还看到了隐式参数 this,所以在平时的编码过程中我们可以在方法体中使用 this 变量。看到这里就知道在获取方法参数名称时应该采用哪种方案了。

Reference

Obtaining Names of Method Parameters
JEP 118: Access to Parameter Names at Runtime
java 8 - Drawbacks of javac -parameters flag - Stack Overflow
Chapter 4. The class File Format - 4.7.13. The LocalVariableTable Attribute