Java 8:通过反射获取方法参数名

Published: 15 Apr 2014 Category: Java

JDK 8中一个很少被提及的特性就是它可以方法参数的元信息存储到编译完的class文件中(JEP 118)。这个特性使得Java程序可以在运行时通过反射来获取参数的元信息。

在Java教程——反射API中有节课叫获取方法参数的名字,里面讲到了如何使用Java 8的这个新特性。这个教程提供了一个MethodParameterSpy类,你可以用它来检测某个Java类的方法参数的属性。文中同时提及,将额外的参数信息存储在.class文件里面是一个可选的特性,因为这会增加class文件的大小。它同时还指出,有些情况下参数名内包含一些敏感信息,因此开发人员并不希望将它保存到编译后的.class文件中。

在Java 8中,使用javac编译器的时候加上-parameters参数的话,会在生成的.class文件中额外存储参数的元信息。当你输入javac -help的时候,你会看到-parameters这个选项,就像下面的截图中那样。

Oracle在javac的官方文档中,讲到了如何在运行时获取这个额外的方法参数元信息:"将方法及构造方法的参数存储在生成的.class文件中,这样可以通过反射API java.lang.reflect.Executable.getParameters来获取到这些信息"。下面的代码片段(ParameterDisplayer类)将会展示这个功能。

package dustin.examples.jdk8;
 
import static java.lang.System.out;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
 
/**
 * Uses JDK 8 Parameter class to demonstrate metadata related to the parameters
 * of the methods and constructors of the provided class (includes private,
 * protected, and public methods, but does not include methods inherited from
 * parent classes; those classes should be individually submitted).
 * 
 * @author Dustin
 */
public class ParameterDisplayer
{
   private static void displayParametersMetadata(final String[] classesNames)
   {
      for (final String className : classesNames)
      {
         try
         {
            final Class clazz = Class.forName(className);
 
            // Get all class's declared methods (does not get inherited methods)
            final Method[] declaredMethods = clazz.getDeclaredMethods();
            for (final Method method : declaredMethods)
            {
               writeHeader(
                    "Method " + method.toGenericString()
                  + " has " + method.getParameterCount() + " Parameters:");
               int parameterCount = 0;
               final Parameter[] parameters = method.getParameters();
               for (final Parameter parameter : parameters)
               {
                  out.println(
                       "\targ" + parameterCount++ + ": "
                     + (parameter.isNamePresent() ? parameter.getName() : "Parameter Name not provided,")
                     + (isParameterFinal(parameter) ? " IS " : " is NOT ")
                     + "final, type " + parameter.getType().getCanonicalName()
                     + ", and parameterized type of " + parameter.getParameterizedType()
                     + " and " + (parameter.isVarArgs() ? "IS " : "is NOT ")
                     + "variable." );
               }
            }
         }
         catch (ClassNotFoundException cnfEx)
         {
            out.println("Unable to find class " + className);
         }
      }
   }
 
   private static void writeHeader(final String headerText)
   {
      out.println("\n==========================================================");
      out.println("= " + headerText);
      out.println("==========================================================");
   }
 
   /**
    * Indicate whether provided Parameter is final.
    * 
    * @param parameter Parameter to be tested for 'final' modifier.
    * @return {@code true} if provided Parameter is 'final'.
    */
   private static boolean isParameterFinal(final Parameter parameter)
   {
      return Modifier.isFinal(parameter.getModifiers());
   }
 
   public static void main(final String[] arguments)
   {
      if (arguments.length < 1)
      {
         out.println("You must provide the fully qualified name of at least one class.");
         System.exit(-1);
      }
 
      displayParametersMetadata(arguments);
   }
}

一开始我想用这个程序来测试下JDK里的某个大家比较熟悉的类,不过后来想了下可能没用,因为JDK的类编译的时候应该是没加-parameters选项。因此为了演示的需要,我自己又简单地写了一个类。

package dustin.examples.jdk8;
 
import java.util.List;
 
/**
 * Class with numerous methods intended to be used in demonstrating JDK 8's new
 * Parameter class.
 *
 * @author Dustin
 */
public class ManyMethods
{
   public ManyMethods() {}
 
   private void addArrayOfStrings(String[] strings) {}
 
   private void addManyStrings(final String ... strings) {}
 
   private void addListOfStrings(final List<String> strings) {}
 
   @Override
   public String toString()
   {
      return "ManyMethods";
   }
}

下面两个截图演示的是没加-parameters选项和加了这个选项的时候,用ParameterDisplayer来运行ManyMethods类的结果。最明显的区别就是,没加-parameters选项的时候,参数名是无法获取到的。还有就是如果没加这个选项,参数是否是final类型的是不确定的。如果没加-parameters选项的话,不管参数是不是final类型的,Parameter.getModifiers()返回的结果都是不包含final的。

ParameterDisplayer类使用了Parameter.isNamePresent()来自动检测参数名是否存在(是否用-parameters选项编译了)。不这么做的话,如果参数名不存在,Parameter.getName()返回的是"arg"加上参数的序号(第一个参数的话是arg0,第二个arg1,依次类推)。

ManyMethods类里有两个方法都包含一个final类型的参数。只有当编译的时候加上-parameters选项,才能通过Parameter.getModifiers()方法来正确的识别出是否是final类型。

一点题外话:Sun/Oracle的工具文档中通常包含一个"windows"页及"solaris"页,后者通常用来说明如何在Linux/Unix的各个平台上运行某个工具。我注意到在Java 8的文档中这个已经变了。文档仍然有一个windows版本,但Unix/Linux版本的URL现在标识的是"unix"了。为了说明这点,下面列举了Java SE7和Java SE8里javac工具的文档页的URL:

+http://docs.oracle.com/javase/8/docs/technotes/tools/windows/javac.html +http://docs.oracle.com/javase/8/docs/technotes/tools/unix/javac.html +http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html +http://docs.oracle.com/javase/7/docs/technotes/tools/solaris/javac.html

再回到Parameter类的话题上,值得注意的是存储额外的参数元信息的.class文件的大小会增加。就上面这个ManyMethods类而言,.class文件从909字节增加到了961字节。

Constructor,和Method一样,都继承了Executable接口,因此Constructor类和Methods类一样,也有一个getParamaters方法。在Java 8里,如果代码编译的时候增加了这个额外的信息的话,你能获取到更详细的关于方法参数的信息。

原创文章转载请注明出处:Java 8:通过反射获取方法参数名

英文原文链接