资讯专栏INFORMATION COLUMN

Java 运行时获取方法参数名

cfanr / 3428人阅读

摘要:原文如果觉得我的文章对你有用,请随意赞赏本文整理运行时获取方法参数名的两种方法,的最新的方法和之前的方法。文件中的调试信息上文介绍了通过新增的反射运行时获取方法参数名。

原文:http://nullwy.me/2017/04/java...
如果觉得我的文章对你有用,请随意赞赏

本文整理 Java 运行时获取方法参数名的两种方法,Java 8 的最新的方法和 Java 8 之前的方法。

Java 8 的新特性

翻阅 Java 8 的新特性,可以看到有这么一条“JEP 118: Access to Parameter Names at Runtime”。这个特性就是为了能运行时获取参数名新加的。这个 JEP 只是功能增强的提案,并没有最终实现的 JDK 相关的 API 的介绍。查看“Enhancements to the Reflection API” 会看到如下介绍:

Enhancements in Java SE 8
Method Parameter Reflection: You can obtain the names of the formal parameters of any method or constructor with the method java.lang.reflect.Executable.getParameters. However, .class files do not store formal parameter names by default. To store formal parameter names in a particular .class file, and thus enable the Reflection API to retrieve formal parameter names, compile the source file with the -parameters option of the javac compiler.

javac 文档中关于 -parameters 的介绍如下 [doc man ]:

-parameters
Stores formal parameter names of constructors and methods in the generated class file so that the method java.lang.reflect.Executable.getParameters from the Reflection API can retrieve them.

现在试验下这个特性。有如下两个文件:

package com.test;

public class TestClass {
    public int sum(int num1, int num2) {
        return num1 + num2;
    }
}
package com.test;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class Java8Main {
    public static void main(String[] args) throws NoSuchMethodException {
        Method method = TestClass.class.getDeclaredMethod("sum", int.class, int.class);
        Parameter[] parameters = method.getParameters();
        for (Parameter parameter : parameters) {
            System.out.println(parameter.getType().getName() + " " + parameter.getName());
        }
    }
}

先试试 javac 不加 -parameters 编译,结果如下:

$ javac -d "target/classes" src/main/java/com/test/*.java
$ java -cp "target/classes" com.test.Java8Main
int arg0
int arg1

加上 -parameters 后,运行结果如下:

$ javac -d "target/classes" -parameters src/main/java/com/test/*.java
$ java -cp "target/classes" com.test.Java8Main
int num1
int num2

可以看到,加上 -parameters 后,正确获得了参数名。实际开发中,很少直接用命令行编译 Java 代码,项目一般都会用 maven 管理。在 maven 下,只需修改 pom 文件的 maven-compiler-plugin 插件配置即可,就是加上了 compilerArgs 节点 [doc ],如下:


    org.apache.maven.plugins
        maven-compiler-plugin
        
        1.8
        1.8
        
            -parameters
        
    
实现原理

“Enhancements in Java SE 8”提到,参数名信息回存储在 class 文件中。现在试试用 javap( doc man)命令反编译生成的 class 文件。反编译 class 文件:

$ javap -v -cp "target/classes" com.test.TestClass
Classfile /Users/yulewei/IdeaProjects/hellojava/target/classes/com/test/TestClass.class
  Last modified 2017-5-2; size 305 bytes
  MD5 checksum 24b99fec7f3062f5de1c3ca4270a1d36
  Compiled from "TestClass.java"
public class com.test.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#15         // java/lang/Object."":()V
   #2 = Class              #16            // com/test/TestClass
   #3 = Class              #17            // java/lang/Object
   #4 = Utf8               
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               sum
   #9 = Utf8               (II)I
  #10 = Utf8               MethodParameters
  #11 = Utf8               num1
  #12 = Utf8               num2
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #4:#5          // "":()V
  #16 = Utf8               com/test/TestClass
  #17 = Utf8               java/lang/Object
{
  public com.test.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int sum(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 6: 0
    MethodParameters:
      Name                           Flags
      num1
      num2
}
SourceFile: "TestClass.java"

在结尾的 MethodParameters 属性就是,实现运行时获取方法参数的核心。这个属性是 Java 8 的 class 文件新加的,具体介绍可以参考官方“Java 虚拟机官方”文档的介绍,“4.7.24. The MethodParameters Attribute”,doc。

class 文件中的调试信息

上文介绍了 Java 8 通过新增的反射 API 运行时获取方法参数名。那么在 Java 8 之前,有没有办法呢?或者在编译时没有开启 -parameters 参数,又如何动态获取方法参数名呢?其实 class 文件中保存的调试信息就可以包含方法参数名。

javac-g 选项可以在 class 文件中生成调试信息,官方文档介绍如下 [doc man ]:

-g
Generates all debugging information, including local variables. By default, only line number and source file information is generated.
-g:none
Does not generate any debugging information.
-g:[keyword list]
Generates only some kinds of debugging information, specified by a comma separated list of keywords. Valid keywords are:
   source
     Source file debugging information.
   lines
     Line number debugging information.
   vars
     Local variable debugging information.

可以看到默认是包含源代码信息和行号信息的。现在试验下不生成调试信息的情况:

$ javac -d "target/classes" src/main/java/com/test/*.java -g:none
$ javap -v -cp "target/classes" com.test.TestClass
Classfile /Users/yulewei/IdeaProjects/hellojava/target/classes/com/test/TestClass.class
  Last modified 2017-5-2; size 177 bytes
  MD5 checksum 559f5448154e4d7dd089f8155d8d0f55
public class com.test.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#9          // java/lang/Object."":()V
   #2 = Class              #10            // com/test/TestClass
   #3 = Class              #11            // java/lang/Object
   #4 = Utf8               
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               sum
   #8 = Utf8               (II)I
   #9 = NameAndType        #4:#5          // "":()V
  #10 = Utf8               com/test/TestClass
  #11 = Utf8               java/lang/Object
{
  public com.test.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return

  public int sum(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
}

对比上文的反编译结果,可以看到,输出结果中的 Compiled from "TestClass.java" 没了,Constant pool 中也不再有 LineNumberTableSourceFilecode 属性里的 LocalVariableTable 属性也没了(当然,因为编译时没加 -parameters 参数,MethodParameters 属性自然也没了)。若选择不生成这两个属性,对程序运行产生的最主要的影响就是,当抛出异常时,堆栈中将不会显示出错代码所属的文件名和出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

$ javac -d "target/classes" src/main/java/com/test/*.java -g:vars
$ javap -v -cp "target/classes" com.test.TestClass
Classfile /Users/yulewei/IdeaProjects/hellojava/target/classes/com/test/TestClass.class
  Last modified 2017-5-2; size 302 bytes
  MD5 checksum d430f817e0e2cfafc9095279c67aaa72
public class com.test.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#15         // java/lang/Object."":()V
   #2 = Class              #16            // com/test/TestClass
   #3 = Class              #17            // java/lang/Object
   #4 = Utf8               
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LocalVariableTable
   #8 = Utf8               this
   #9 = Utf8               Lcom/test/TestClass;
  #10 = Utf8               sum
  #11 = Utf8               (II)I
  #12 = Utf8               num1
  #13 = Utf8               I
  #14 = Utf8               num2
  #15 = NameAndType        #4:#5          // "":()V
  #16 = Utf8               com/test/TestClass
  #17 = Utf8               java/lang/Object
{
  public com.test.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/test/TestClass;

  public int sum(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/test/TestClass;
            0       4     1  num1   I
            0       4     2  num2   I
}

可以看到,code 属性里的出现了 LocalVariableTable 属性,这个属性保存的就是方法参数和方法内的本地变量。在演示代码的 sum 方法中没有定义本地变量,若存在的话,也将会保存在 LocalVariableTable 中。

javap-v 选项会输出全部反编译信息,若只想看行号和本地变量信息,改用 -l 即可。输出结果如下:

$ javap -l -cp "target/classes" com.test.TestClass
public class com.test.TestClass {
  public com.test.TestClass();
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   Lcom/test/TestClass;

  public int sum(int, int);
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       4     0  this   Lcom/test/TestClass;
          0       4     1  num1   I
          0       4     2  num2   I
}

若要全部生成全部提示信息,编译参数需要改为 -g:source,lines,vars。一般在 IDE 下调试代码都需要调试信息,所以这三个参数默认都会开启。IDEA 下的 javac 默认参数设置,如图:

若使用 maven,maven 的默认的编译插件 maven-compiler-plugin 也会默认开启这三个参数 [doc],经实际验证也包括了LocalVariableTable

代码如何实现

上文中讲了 class 文件中的调试信息中 LocalVariableTable 属性里就包含方法名参数,这就是运行时获取方法参数名的方法。读取这个属性,JDK 并没有提供 API,只能借助第三方库解析 class 文件实现。

要解析 class 文件典型的工具库有 ObjectWeb 的 ASM(wiki,home,mvn,javadoc)、Apache 的 Commons BCEL(wiki,home,mvn,javadoc)、 日本教授开发的 Javassist(wiki,github,mvn,javadoc)等。其中 ASM 使用最广,使用 ASM 的知名开源项目有,AspectJ, CGLIB, Clojure, Groovy, JRuby, Jython, TopLink等等 [ref ]。当然使用 BCEL 的项目也很多 [ref ]。ASM 相对其他库的 jar 更小,运行速度更快 [javadoc ]。目前 asm-5.0.1.jar 文件大小 53 KB,BCEL 5.2 版本文件大小 520 KB,javassist-3.20.0-GA.jar 文件大小 751 KB。jar 包文件小,自然意味着代码量更少,提供的功能自然也少了。

BCEL

先来看看用 BCEL 获取方法参数名的写法,代码如下:

package com.test;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LocalVariable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.Type;

public class BcelMain {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
        java.lang.reflect.Method m = TestClass.class.getDeclaredMethod("sum", int.class, int.class);
        JavaClass clazz = Repository.lookupClass("com.test.TestClass");
        Method bcelMethod = clazz.getMethod(m);
        LocalVariableTable lvt = bcelMethod.getLocalVariableTable();
        for (LocalVariable lv : lvt.getLocalVariableTable()) {
            System.out.println(lv.getName() + "  " + lv.getSignature() + "  " + Type.getReturnType(lv.getSignature()));
        }
    }
}

输出结果:

this  Lcom/test/TestClass;  com.test.TestClass
num1  I  int
num2  I  int
ASM

ASM 的写法如下:

package com.test;
import org.objectweb.asm.*;

public class AsmMain {

    public static void main(String[] args) throws Exception {
        ClassReader classReader = new ClassReader("com.test.TestClass");
        classReader.accept(new ParameterNameDiscoveringVisitor("sum", "(II)I"), 0);
    }

    private static class ParameterNameDiscoveringVisitor extends ClassVisitor {
        private final String methodName;
        private final String methodDesc;

        public ParameterNameDiscoveringVisitor(String name, String desc) {
            super(Opcodes.ASM5);
            this.methodName = name;
            this.methodDesc = desc;
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            if (name.equals(this.methodName) && desc.equals(methodDesc))
                return new LocalVariableTableVisitor();
            return null;
        }
    }

    private static class LocalVariableTableVisitor extends MethodVisitor {

        public LocalVariableTableVisitor() {
            super(Opcodes.ASM5);
        }

        @Override
        public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) {
            System.out.println(name + "  " + description);
        }
    }
}
Spring 框架

若使用 Spring 框架,对于运行时获取参数名,Spring 提供了内建支持,对应的实现类为 DefaultParameterNameDiscoverer (javadoc)。该类先尝试用 Java 8 新的反射 API 获取方法参数名,若无法获取,则使用 ASM 库读取 class 文件的 LocalVariableTable,对应的代码分别为 StandardReflectionParameterNameDiscoverer 和 LocalVariableTableParameterNameDiscoverer。

参考资料

2014-10 Java 8 Named Method Parameters https://www.beyondjava.net/bl...

JEP 118: Access to Parameter Names at Runtime http://openjdk.java.net/jeps/118

Enhancements to the Reflection API http://docs.oracle.com/javase...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74630.html

相关文章

  • 【深度好文】深度分析如何获取方法参数

    摘要:但是这种方式对于接口和抽象方法是不管用的,因为抽象方法没有方法体,也就没有局部变量,自然也就没有局部变量表了是通过接口跟语句绑定然后生成代理类来实现的,因此它无法通过解析字节码来获取方法参数名。 声明:本文属原创文章,首发于公号:程序员自学之道,转载请注明出处! 发现问题 对Java字节码有一定了解的朋友应该知道,Java 在编译的时候,默认会将方法参数名丢弃,因此我们无法在运行时获取...

    vslam 评论0 收藏0
  • JAVA 8 反射获取参数

    摘要:前言在之前编译是不会把构造器和方法的参数名编译进中,如果需要获取参数名,可以在方法上加上注解,反射获取注解的值从而获取参数名,比如的和。带在中添加命令行在后面加运行结果构造器方法一方法二方法三方法四这样就把参数名给打印出来了,为。 前言 在JDK8之前javac编译是不会把构造器和方法的参数名编译进class中,如果需要获取参数名,可以在方法上加上注解,反射获取注解的值从而获取参数名,...

    shiweifu 评论0 收藏0
  • Java通过ASM运行读取方法参数

    摘要:据说已经原生支持参数名读取了。本文以为例进行说明通过字节码操作工具我们可以实现运行时参数名的读写。简单说说原理字节码为每个方法保存了一份方法本地变量列表。 据说Java8已经原生支持参数名读取了。具体不是很清楚。本文以java7为例进行说明.通过ASM字节码操作工具我们可以实现运行时参数名的读写。简单说说原理:java字节码为每个方法保存了一份方法本地变量列表。可以通过ASM获取这个列...

    DangoSky 评论0 收藏0
  • 1、类加载器 2、反射构造方法 3、反射成员变量 4、反射成员方法 5、反射配置文件运行类中的方法

    摘要:通过反射获取无参构造方法并使用得到无参构造方法获取所有的修饰的构造方法。如果方法没有返回值,返回的是反射获取空参数成员方法并运行代码演示反射获取成员方法并运行获取对象中的成员方法获取的是文件中的所有公共成员方法包括继承的类是描述 01类加载器 * A.类的加载 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化。  ...

    Profeel 评论0 收藏0
  • 一文带你了解Java反射机制

    摘要:在的反射包中提供了三个类以及来分别描述属性方法和构造器。获取构造器获取方法可以看到我们可以通过一个类的对象很轻松的获取他的属性构造器以及方法信息。返冋一个用于描述构造器名的字符串。 想要获取更多文章可以访问我的博客 - 代码无止境。 上周上班的时候解决一个需求,需要将一批数据导出到Excel。本来公司的中间件组已经封装好了使用POI生成Excel的工具方法,但是无奈产品的需求里面有个合...

    darcrand 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<