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.