• JAVA如何通过反射获取参数名?
  • 发布于 2个月前
  • 125 热度
    0 评论
在JDK8 之前,反射是不能取到参数名称的。 在JDK增强意见:JPE 118:Access to Parameter Names at Runtime中指出,在Java8中我们终于可以通过反射来获取方法的参数名,其主要的目的是:
1.提高代码的可读性(原先通常使用注解来实现)

2.可以提高IDE的功能


JDK8前获取参数名的方法
通过注解来实现
因为Java8之前不提供获取参数名称的功能,大部分实现都是通过提供注解元数据来标明参数名,这个也是目前许多框架使用的方法,如,SpringMVC的参数绑定,MyBatis的参数映射,类似:
//自定义@param注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Param {
    String value();
}
//声明参数名
public void foo(@Param("name") String name, @Param("count") int count){
     System.out.println("name:=" + name + ",count=" + count);
}

//获取
Method foo = ParameterDemo.class.getMethod("foo", String.class, int.class);

Annotation[][] parameterAnnotations = foo.getParameterAnnotations();

for (Annotation[] parameterAnnotation : parameterAnnotations) {
    for (Annotation annotation : parameterAnnotation) {
        if(annotation instanceof Param){
            System.out.println(((Param) annotation).value());
        }
    }
}
//获取结果
name
count
通过解析class文件
可以通过解析二进制文件来获取参数的名称,常见的工具有:ASM,javassist,如,Spring的LocalVariableTableParameterNameDiscoverer,就是利用ASM通过class文件中的本地方法变量表中获取到参数名称:
//使用Spring的LocalVariableTableParameterNameDiscoverer获取
public void foo(String name, int count){
    System.out.println("name:=" + name + ",count=" + count);
}

Method foo = ParameterDemo.class.getMethod("foo", String.class, int.class);
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(foo);
        System.out.println(Arrays.toString(parameterNames));

//获取结果
[name, count]

使用Java8反射
JDK8在反射包中新增了Parameter类,用于表示方法的参数信息,通过Method来获取所有参数列表:
public void foo(String name, int count){
    System.out.println("name:=" + name + ",count=" + count);
}
//堆代码 duidaima.com
//通过反射获取
Method foo = ParameterDemo.class.getMethod("foo", String.class, int.class);
Parameter[] parameters = foo.getParameters();
for (Parameter parameter : parameters) {
    System.out.println(parameter.getName());
}
//获取结果
name
count
【注意】
该功能需要在javac编译时开启-parameters参数,而为了兼容性该参数默认是不开启的,如果使用Maven构建的话可以如此配置:
<plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-compiler-plugin</artifactId>
     <configuration>
        <source>8</source>
        <target>8</target>
        <compilerArgs>
            <compilerArg>-parameters</compilerArg>
        </compilerArgs>
     </configuration>
</plugin>
【原理】
通过反编译可以知道,class文件保存了MethodParameters的信息,这就是保存方法名的地方:
//截取片段
  public void foo(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=3
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String name:=
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_1
        16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: ldc           #7                  // String ,count=
        21: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: iload_2
        25: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        28: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        31: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        34: return
      LineNumberTable:
        line 14: 0
        line 15: 34
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      35     0  this   Lcom/sevenlin/demo/reflect/ParameterDemo;
            0      35     1  name   Ljava/lang/String;
            0      35     2 count   I
    MethodParameters:
      Name                           Flags
      name
      count

典型的例子
在springboot actuator里有个典型的例子
@Component
@Endpoint(id = "my", enableByDefault = true) //设置 id,并选择是否默认开启
public class MyEndPoint {

    @ReadOperation
    public List<String> getPaths() {
        List<String> list = new ArrayList<>();
        list.add("java");
        list.add("c++");
        list.add("python");
        return list;
    }

    @ReadOperation
    public String get(@Selector String arg0) {
        return arg0;
    }

    @WriteOperation
    public String post() {
        return "post";
    }

    @DeleteOperation
    public Integer delete() {
        return 1;
    }
}
上面的 Endpoint 有一个 @Selector 参数的方法,并且参数名是 arg0,这个参数名是有学问滴. 如果改成一个有意义的名字是可以的吗?原来给的参数名是 path,原来设想可以访问 /actuator/my/[任意字符] 的路径,但是会报 400 参数不匹配错误。但是,/actuator/my/[任意字符]?path=[任意字符] 是正常访问的。

原来,为了使 @Selector 正常工作,必须使用嵌入的参数名称编译 Endpoint(-parameters),如下。或者将参数名改为 arg0 就能达到目的。
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
            <compilerArgs>
                <arg>-parameters</arg>
            </compilerArgs>
        </configuration>
    </plugin>
或者:
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
            <parameters>true</parameters>
        </configuration>
    </plugin> -->
</plugins></pre>
原因就是spring解析参数的类为
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
    public DefaultParameterNameDiscoverer() {
        if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {
            this.addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
        }

        this.addDiscoverer(new StandardReflectionParameterNameDiscoverer());
        this.addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
    }
}
它的解析顺序为StandardReflectionParameterNameDiscoverer->LocalVariableTableParameterNameDiscoverer
    public String[] getParameterNames(Method method) {
        Iterator var2 = this.parameterNameDiscoverers.iterator();

        String[] result;
        do {
            if (!var2.hasNext()) {
                return null;
            }

            ParameterNameDiscoverer pnd = (ParameterNameDiscoverer)var2.next();
            result = pnd.getParameterNames(method);
        } while(result == null);

        return result;
    }
从代码上看,StandardReflectionParameterNameDiscoverer 比LocalVariableTableParameterNameDiscoverer先解析。在由于在JDK 缺省编译情况下,class 不包含parameter 名字信息,因此StandardReflectionParameterNameDiscoverer只有arg0 可以辨认,但是确实能够解析出来,因此LocalVariableTableParameterNameDiscoverer就用不到了。 如果加入编译参数,StandardReflectionParameterNameDiscoverer也可以解析出来相应的参数名称。

从代码可以看到,如果StandardReflectionParameterNameDiscoverer 和LocalVariableTableParameterNameDiscoverer顺序反一反,即使不加编译参数也是可以解析正确的,但是这个效率就很差。需要扫描方法区类定义的方法表,来获得参数名称。

总结
Java运行时是使用位置解析参数的,但随着应用的发展,常常需要获取参数名的功能,如一些IDE的反编译功能,由于Java8之前不支持参数名的获取,通常都是通过别的方法来实现,通常就是通过注解,这会使代码比较混乱,而且冗余,降低代码阅读性,而通过解析class文件的方式来获取比较麻烦,而且也不提倡。

Java8对反射的增加也是为了解决以上的问题,而使用的时候注意要在编译时开启。
用户评论