Java 運行時獲取方法參數名

原文: http://nullwy.me/2017/04/java...
若是以爲個人文章對你有用,請隨意讚揚

本文整理 Java 運行時獲取方法參數名的兩種方法,Java 8 的最新的方法和 Java 8 以前的方法。html

Java 8 的新特性

翻閱 Java 8 的新特性,能夠看到有這麼一條「JEP 118: Access to Parameter Names at Runtime」。這個特性就是爲了能運行時獲取參數名新加的。這個 JEP 只是功能加強的提案,並無最終實現的 JDK 相關的 API 的介紹。查看「Enhancements to the Reflection API」 會看到以下介紹:java

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 ]:git

-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.

如今試驗下這個特性。有以下兩個文件:github

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 編譯,結果以下:web

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

加上 -parameters 後,運行結果以下:spring

$ 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 ],以下:shell

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
    </configuration>
</plugin>

實現原理

「Enhancements in Java SE 8」提到,參數名信息回存儲在 class 文件中。如今試試用 javap doc man)命令反編譯生成的 class 文件。反編譯 class 文件:apache

$ 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."<init>":()V
   #2 = Class              #16            // com/test/TestClass
   #3 = Class              #17            // java/lang/Object
   #4 = Utf8               <init>
   #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          // "<init>":()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."<init>":()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」,docc#

class 文件中的調試信息

上文介紹了 Java 8 經過新增的反射 API 運行時獲取方法參數名。那麼在 Java 8 以前,有沒有辦法呢?或者在編譯時沒有開啓 -parameters 參數,又如何動態獲取方法參數名呢?其實 class 文件中保存的調試信息就能夠包含方法參數名。api

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."<init>":()V
   #2 = Class              #10            // com/test/TestClass
   #3 = Class              #11            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               sum
   #8 = Utf8               (II)I
   #9 = NameAndType        #4:#5          // "<init>":()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."<init>":()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."<init>":()V
   #2 = Class              #16            // com/test/TestClass
   #3 = Class              #17            // java/lang/Object
   #4 = Utf8               <init>
   #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          // "<init>":()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."<init>":()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 默認參數設置,如圖:

IDEA 默認的 javac 設置

若使用 maven,maven 的默認的編譯插件 maven-compiler-plugin 也會默認開啓這三個參數 [doc],經實際驗證也包括了LocalVariableTable

代碼如何實現

上文中講了 class 文件中的調試信息中 LocalVariableTable 屬性裏就包含方法名參數,這就是運行時獲取方法參數名的方法。讀取這個屬性,JDK 並無提供 API,只能藉助第三方庫解析 class 文件實現。

要解析 class 文件典型的工具庫有 ObjectWeb 的 ASM(wikihomemvnjavadoc)、Apache 的 Commons BCEL(wikihomemvnjavadoc)、 日本教授開發的 Javassist(wikigithubmvnjavadoc)等。其中 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 提供了內建支持,對應的實現類爲 DefaultParameterNameDiscovererjavadoc)。該類先嚐試用 Java 8 新的反射 API 獲取方法參數名,若沒法獲取,則使用 ASM 庫讀取 class 文件的 LocalVariableTable,對應的代碼分別爲 StandardReflectionParameterNameDiscovererLocalVariableTableParameterNameDiscoverer

參考資料

相關文章
相關標籤/搜索