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 extends TypeElement> 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
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
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.
Wow – that’s cool. I hadn’t seen Lombok before. It’s basically the same as Groovy’s AST Transformations.