前言

之前在看公司其他小组关于定时补偿机制的相关代码,发现了这么两行代码:

private static final LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
......
//获取方法参数名数组
String[] parameterNames = discoverer.getParameterNames(method);

这是什么API?可以获取方法参数名?记得只有在jdk8且加了-paramters编译参数的情况下才能用反射API获取方法参数名。这引起了我的好奇~ 看了下它的package:org.springframework.core.LocalVariableTableParameterNameDiscoverer;来自spring-core模块,这让我联想起来在使用Spring MVC的时候,你即使不使用注解,只要参数名和请求参数的key对应上了,就能自动完成数值的封装。而在使用MyBatis(接口模式)时,接口方法向xml里的SQL语句传参时,必须(当然不是100%的必须,特殊情况此处不做考虑)使用@Param(’’)指定key值,在SQL中才可以取到。

问题发现

java使用者都知道,.java文件属于源码文件,它需要经过了javac编译器编译为.class字节码文件才能被JVM执行的。 对.class字节码稍微有点了解的小伙伴应该也知道这一点:Java在编译的时候对于方法,默认是不会保留方法参数名,因此如果我们在运行期想从.class字节码里直接拿到方法的参数名是做不到的。 如下案例,获取不到真实参数名:

public static void main(String[] args) throws NoSuchMethodException {
    Method method = Main.class.getMethod("test", String.class, Integer.class);
    int parameterCount = method.getParameterCount();
    Parameter[] parameters = method.getParameters();

    // 打印输出:
    System.out.println("方法参数总数:" + parameterCount);
    Arrays.stream(parameters).forEach(p -> System.out.println(p.getType() + "----" + p.getName()));
}

打印内容:

方法参数总数:2
class java.lang.String----arg0
class java.lang.Integer----arg1

从结果中可以看到我们并不能获取到真实方法参数名(获取到的是无意义的arg0、arg1)

这个时候你应该有这样的疑问:在使用Spring MVC的时候,Controller的方法中不使用注解一样可以自动封装啊,形如这样:

@GetMapping("/test")
public Object test(String name, Integer age) {
    String value = name + "---" + age;
    System.out.println(value);
    return value;
}

请求:/test?name=wlj&age=18。控制台输出:

wlj---18

再看此例(还原Spring MVC获取参数名的场景):

public class Main {

    public String testArgName(String name,Integer age){
        return null;
    }

    public static void main(String[] args) throws NoSuchMethodException {
        Method method = Main.class.getMethod("testArgName", String.class, Integer.class);
        int parameterCount = method.getParameterCount();
        Parameter[] parameters = method.getParameters();

        // 打印输出:
        System.out.println("方法参数总数:" + parameterCount);
        Arrays.stream(parameters).forEach(p -> System.out.println(p.getType() + "----" + p.getName()));

        MethodParameter nameParameter = new MethodParameter(method, 0);
        MethodParameter ageParameter = new MethodParameter(method, 1);
        ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
        nameParameter.initParameterNameDiscovery(parameterNameDiscoverer);
        ageParameter.initParameterNameDiscovery(parameterNameDiscoverer);
        System.out.println(nameParameter.getParameterType() + "----" + nameParameter.getParameterName());
        System.out.println(ageParameter.getParameterType() + "----" + ageParameter.getParameterName());
    }
}

输出结果:

方法参数总数:2
class java.lang.String----arg0
class java.lang.Integer----arg1
class java.lang.String----name
class java.lang.Integer----age

从结果能看出来:Spring MVC借助ParameterNameDiscoverer完成了方法参数名的获取,进而完成数据封装


为了便于理解,先简单说说字节码中的两个概念:LocalVariableTable和LineNumberTable,本文关注的焦点是LocalVariableTable,但也借此机会一笔带过LineNumberTable

LineNumberTable 我之前有过疑问:线上程序抛出异常时显示的行号,为啥就恰好就是你源码的那一行呢?有这疑问是因为JVM执行的是.class文件,而该文件的行和.java源文件的行肯定是对应不上的,为何行号却能在.java文件里对应上? 这就是LineNumberTable它的作用了:LineNumberTable属性存在于代码的字节码中, 它建立了字节码偏移量到源代码行号之间的联系

LocalVariableTable

LocalVariableTable属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系。这个属性也是存在于代码的字节码中~ 从名字可以看出来:它是局部变量的一个集合。描述了局部变量和描述符以及和源代码的对应关系。

下面我使用javac和javap命令来演示一下这个情况: java源码如下:

1	import java.lang.reflect.Method;
2	import java.lang.reflect.Parameter;
3
4	/**
5 	* @Author lijian.wu
6 	* @Date 2021/6/20 7:32 下午
7 	*/
8	public class Main2 {
9
10    public static void main(String[] args) throws NoSuchMethodException {
11      Method method = Main2.class.getMethod("testArgName", String.class, 12Integer.class);
13      System.out.println("paramCount:" + method.getParameterCount());
14        for (Parameter parameter : method.getParameters()) {
15            System.out.println(parameter.getType().getName() + "-->" + parameter.getName());
16        }
17    }
18
19    public String testArgName(String name,Integer age){
20        return null;
21    }
22}

依次执行javac Main2.java 和 javap -verbose Main2.class 查看字节码信息如下:

public class Main2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  ...
{
  public Main2();
    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 8: 0

  public static void main(java.lang.String[]) throws java.lang.NoSuchMethodException;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=6, locals=6, args_size=1
        ..
        LineNumberTable:
        line 11: 0
        line 12: 22
        line 13: 50
        line 14: 73
        line 13: 113
        line 16: 119
      ...

  public java.lang.String testArgName(java.lang.String, java.lang.Integer);
    descriptor: (Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=3
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 19: 0
}

从上的LineNumberTable可看到,line行号和源码处完全一样,这就解答了我们上面的行号对应的疑问了:LineNumberTable它记录着在源代码处的行号。 注意:此处并没有LocalVariableTable

然后分别使用javac -parameters、javac -g来编译后再执行,结果图如下:

spring 参数 字符串截取 spring获取参数名_java

另外附上-parameters、-g编译后增加的字节码信息:-parameters:

spring 参数 字符串截取 spring获取参数名_spring 参数 字符串截取_02

-g:

spring 参数 字符串截取 spring获取参数名_springmvc_03

这里多了一个LocalVariableTable,即局部变量表,就记录着我们方法入参的形参名字。既然记录着了,这样我们就可以通过分析字节码信息来得到这个名称了~


获取方法参数名的2种方式介绍

虽然Java编译器默认情况下会抹去方法的参数名,但有上面介绍了字节码的相关知识可知,我们还是有方法来得到方法的参数名的。下面介绍3个方案,供以参考。

方法一:使用-parameters(最简单直接) java8原生支持,直接通过java.lang.reflect.Parameter就能获取到

方法二:借助ASM(推荐) 说到ASM,小伙伴们至少对这个名字应该是不陌生的。它是一个Java字节码操控框架,它能被用来动态生成类或者增强既有类的功能,它能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。 这里我们就可以通过ASM来分析字节码信息中的本地变量表获取方法参数名了。因为ASM的api使用很复杂我这里就不演示了,大家有兴趣自己了解下~

那么言归正传,来解释文首的疑问:Q:为什么Spring MVC可以获取方法参数名? A:spring-core中有个ParameterNameDiscoverer就是用来获取参数名的,底层用的是asm解析,但是接口方法的参数名无法得到,即只能是非接口类的方法参数名可以。 从文首的例子可以看出Spring MVC它最终依赖的是DefaultParameterNameDiscoverer去帮忙获取到入参名。

备注:Spring已默认引入ASM

spring 参数 字符串截取 spring获取参数名_java_04

可是平时我们开发spring MVC项目都没有显式添加-g编译参数Spring MVC是怎么获取方法参数的? 其实~maven默认是添加了-g选项的

看下maven-compiler-plugin的文档:maven.apache.org/plugin…

注意下插件的这几个参数:

  • 1.<debug>:默认值是true,可以把调试信息添加到class文件中
  • 2.<debuglevel>:定义了出现在-g参数后面的选项,要么啥都不填,要么是以后号分隔的lines, vars, source,如果不设置,默认就是-g,不带其他选项。只有在开启debug有效。

因此,可以看出来,默认情况下,maven-compiler-plugin实际上就是给javac添加了-g参数,也就是如下:

<plugins>
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
      <debug>true</debug>     //默认,无需显式配置
      <compilerArgument>-g</compilerArgument>  //默认,无需显式配置
    </configuration>
  </plugin>
</plugins>

总结一下

  • 1.要想在运行时获取方法的参数名,首先class文件中得有参数名才可以
  • 2.spring mvc底层是通过asm获取的字节码中的方法参数名
  • 3.javac可以通过添加-g或者-parameter选项在class文件中添加参数信息
  • 4.maven编译插件默认添加了-g选项