Array of Parameter Names in Java

My last post suggested an extension to the Java language that I think will be quite helpful. Until such a feature exists, we can fake it by using annotations.

I created an annotation with a processor that creates a new class, one the contains a single static instance of an array of the parameter names from the constructor.

All code on this page released under the same license as the OpenJDK Libraries: GPL with the classpath exception.

First, the annotation itself.

package com.younglogic.annote;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
public @interface NamedParameters {

}

Next the annotation processor. This uses the javac API, which really should become part of the Base Java platform. In order to compile I added /usr/lib/jvm/java-1.7.0/lib/tools.jar to the classpath.

package com.younglogic.annote;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;

import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;

@SupportedAnnotationTypes(value = { "com.younglogic.annote.NamedParameters" })
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class NamedParametersAnnotationProcessor extends AbstractProcessor {

	@Override
	public boolean process(Set annotations,
			RoundEnvironment roundEnv) {
		for (TypeElement element : annotations) {
			for (Element constructor : roundEnv
					.getElementsAnnotatedWith(element)) {

				ClassSymbol classSymbol = (ClassSymbol) constructor
						.getEnclosingElement();
				PackageElement packageElement = (PackageElement) classSymbol
						.getEnclosingElement();

				JavaFileObject jfo;
				try {
					jfo = processingEnv.getFiler().createSourceFile(
							classSymbol.getQualifiedName() + "ParameterNames");
					BufferedWriter bw = new BufferedWriter(jfo.openWriter());
					bw.append("package ");
					bw.append(packageElement.getQualifiedName());
					bw.append(";");
					bw.newLine();
					bw.newLine();
					bw.append("public class " + classSymbol.getSimpleName()
							+ "ParameterNames {");
					bw.newLine();
					bw.append("    static String[] instance = {");
					MethodSymbol symbol = (MethodSymbol) constructor;
					int i = 0;
					for (VarSymbol param : symbol.getParameters()) {
						if (i++ > 0) {
							bw.append(",");
						}
						bw.newLine();
						bw.append("            \"" + param.name + "\"");
					}
					bw.newLine();
					bw.append("        };");
					bw.newLine();
					bw.append("    }");
					bw.newLine();
					bw.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			}

		}
		return true;
	}
}

A sample class that uses this annotation. Note that this class is immutable.

package com.younglogic.annotetest;

import com.younglogic.annote.NamedParameters;

public class SampleClass {

	final String s1;
	final String s2;
	final Integer i1;

	@NamedParameters
	public SampleClass(String s1, String s2, Integer i1) {
		super();
		this.s1 = s1;
		this.s2 = s2;
		this.i1 = i1;
	}

	@Override
	public String toString() {

		return "SampleClass values are: s1=" + s1 + ", s2=" + s2 + ", i1=" + i1.toString();
	}
}

Now add some parameters to the compiler.

-processor is the processor we want to explicitly call
-processorpath tells it where to find the new annotation.
-s tells it where to put the generated code

 javac -cp ../NamedParameters/bin/  -processorpath ../NamedParameters/bin/ -s /tmp/generated/ -processor com.younglogic.annote.NamedParametersAnnotationProcessor   src/com/younglogic/annotetest/SampleClass.java

This will generate the source file: /tmp/generated/com/younglogic/annotetest/SampleClassParameterNames.java

package com.younglogic.annotetest;

public class SampleClassParameterNames {
    public static String[] instance = {
            "s1",
            "s2",
            "i1",
            "i2"
        };
    }

And compile it to /tmp/generated/com/younglogic/annotetest/SampleClassParameterNames.class

We can use that information to create new instances. I have a file SampleClass.properties with the values I want to use to create a new instance.

i1=5
i2=10
s1="first"
s2="next"
 

Read it in and populate a new instance dynamically:

package com.younglogic.annotetest;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;

public class UseParamNames {

	public static void main(String[] args) throws IOException,
			InstantiationException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
		Properties properties = new Properties();

		InputStream instream = UseParamNames.class
				.getResourceAsStream("SampleClass.properties");
		properties.load(instream);

		Constructor c = getConstructor();

		Object[] initargs = new Object[SampleClassParameterNames.instance.length];
		int i = 0;
		for (String name : SampleClassParameterNames.instance) {
			Class[] argTypes = { String.class };

			String strvalue = properties.getProperty(name);
			String[] paramArgs = new String[1];
			paramArgs[0] = strvalue;

			initargs[i] = c.getParameterTypes()[i].getConstructor(argTypes)
					.newInstance(paramArgs);
			i++;

		}

		Object newInstance = c.newInstance(initargs);

		System.out.println(newInstance.toString());
	}

	private static Constructor getConstructor() {
		for (Constructor c : SampleClass.class.getConstructors()) {
			if (c.getAnnotations().length > 0) {
				return c;
			}

		}
		return null;
	}
}

Which, when run, produces the output:

SampleClass values are: s1="first", s2="next", i1=5

3 thoughts on “Array of Parameter Names in Java

  1. Hey Adam:

    Alternatively you can annotative the method parameters themselves. This is the approach that Spring MVC and JAX-RS (@PathParam) and JAX-WS (@WebParam) use. Then the code to get the parameter names from the annotations via reflection at runtime is straightforward. Or you could use Paranamer (http://paranamer.codehaus.org/).

    — Eric

  2. Eric,

    http://paranamer.codehaus.org/ seems right on to me.

    If you annotate each parameter name, you end up repeating yourself. You have to tag each parameter.

    I’ve just stumbled across http://projectlombok.org/ and that seems to me to be the way to go: straightforward manipulations of the AST to provide true Macro functionality in Java. Ideally, I should be able to create immutable objects by simply declaring a list of fields and adding an annotation, which Lombok seems to Support.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.