Monday, April 7, 2014

Constructor/Method Parameters Metadata Available Via Reflection in JDK 8

One of the lesser advertised new features of JDK 8 is the optional ability to include parameter metadata in compiled Java classes [JDK Enhancement Proposal (JEP) 118]. This feature allows Java applications to access this parameter metadata information at runtime via reflection.

The Java Tutorials' Reflection API trail includes a lesson called Obtaining Names of Method Parameters that discusses and demonstrates how to apply this new feature in Java 8. The lesson includes an example Java class MethodParameterSpy that can be run against a provided Java class to indicate characteristics of method and constructor parameters. This lesson also emphasizes that this is an optional feature because storing additional parameter metadata in .class files increases the size of those files. The lesson also points out that there may be some cases where parameter names have sensitive information that the developer does not want available in the compiled .class files.

The additional parameter metadata can be included in .class files compiled in Java 8 by passing the -parameters option to the javac compiler. This -parameters option is also shown when one types javac -help as shown in the next screen snapshot.

The Oracle TechNotes page on javac indicates how this additional method/constructor parameter data can be accessed at runtime: "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." The following code snippet (class called ParameterDisplayer) demonstrates this (emphasis is on the displayParametersMetadata(String[]) method).

ParameterDisplayer.java
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);
   }
}

I had initially thought about running this class against a well-known class of the JDK, but realized that would not be too helpful because those classes are not likely to have been built with the -parameters option. Therefore, I have created a simple example class to aid with the demonstration. It is called ManyMethods and is shown next.

ManyMethods.java
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";
   }
}

The next two screen snapshots demonstrate running ParameterDisplayer against instances of ManyMethods compiled without and with the -parameters option. The most notable differences are that the parameter names are not provided when compiled without the -parameters option. Also, there is no trusted information on whether the parameter is final when compiled without the -parameters option. The Parameter.getModifiers() method does not include final when compiled without -parameters whether or not the parameter is actually final.

The ParameterDisplayer class uses Parameter.isNamePresent() to programmatically identify that the parameter name is not present (when not compiled with the -parameters option). Had that check not been made, the parameter name returned by Parameter.getName() would have been "arg" plus the number of the parameter (arg0 for the first parameter, arg1 for the second parameter, and so on).

Two of the three methods in ManyMethods class that had a parameter had the final modifier on that parameter. These cases were correctly identified by reflection using Parameter.getModifiers() only when the class was compiled with the -parameters option.

Slightly Related Side Note: Sun/Oracle tools documentation has always consisted of a "windows" page and a "solaris" page, with the latter typically being used to describe how the particular tool works on all flavors on Linux and Unix. I noted that this has changed with the Java 8 documentation. This documentation still has a "windows" version, but the Unix/Linux version now has "unix" in its URL. To illustrate this, here are the URLs for Java SE 7 and Java SE 8 javac tool pages:

Returning to the new (with Java 8) Parameter class, it's worth noting that there is an increase in compiled .class files that store this additional parameter metadata. For my ManyMethods class shown above, the .class file was enlarged from 909 bytes to 961 bytes.

Constructor, like Method, extends Executable, and so the Constructor class enjoys the same getParameters method as Method. Java 8 provides more detail on method and constructor parameters when the code is explicitly compiled with that extra information.

1 comment:

@DustinMarx said...

Adam Bien's blog post Named Parameters in Java 8 provides a brief and simple example of the JDK 8 javac -parameters option and the feedback comments are interesting for different perspectives on this feature.